@bytebase/dbhub 1.1.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.
- package/README.md +3 -3
- package/dist/{chunk-GSSLHJB6.js → chunk-CUBIR72I.js} +17 -1
- package/dist/{chunk-EPOKWSFC.js → chunk-PBN2UXB5.js} +1 -1
- package/dist/{chunk-NIILHXTG.js → chunk-QEFMUQJ2.js} +26 -37
- package/dist/{chunk-S2V6RZ3C.js → chunk-RSB7DPO2.js} +64 -3
- package/dist/{chunk-JEZZN2YZ.js → chunk-WIQ4DWGY.js} +16 -0
- package/dist/index.js +72 -25
- package/dist/{mariadb-DUDFBY5W.js → mariadb-E33EXGJZ.js} +7 -9
- package/dist/{mysql-265M3KBD.js → mysql-RLDQI3SJ.js} +56 -11
- package/dist/{postgres-5ZOFYV6X.js → postgres-6XY2GPQV.js} +19 -13
- package/dist/public/assets/{index-DBYlgGks.js → index-DOZ5HOZ9.js} +21 -20
- package/dist/public/index.html +1 -1
- package/dist/{registry-JYRRNC3E.js → registry-XMEJF7WW.js} +2 -2
- package/dist/{sqlite-5M7SDAQZ.js → sqlite-CZE6Z36Y.js} +14 -13
- package/dist/{sqlserver-PSAZRH7G.js → sqlserver-IIGRXGYK.js} +69 -13
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
> [!NOTE]
|
|
2
|
-
> Brought to you by [Bytebase](https://www.bytebase.com/), open-source database
|
|
2
|
+
> Brought to you by [Bytebase](https://www.bytebase.com/), open-source database governance platform.
|
|
3
3
|
|
|
4
4
|
<p align="center">
|
|
5
5
|
<a href="https://dbhub.ai/" target="_blank">
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
MCP Clients MCP Server Databases
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
DBHub is a zero-dependency
|
|
33
|
+
DBHub is a token efficient, zero-dependency MCP server implementing the Model Context Protocol (MCP) server interface. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
|
|
34
34
|
|
|
35
35
|
- **Local Development First**: Zero dependency, token efficient with a minimal set of MCP tools to maximize context window
|
|
36
36
|
- **Multi-Database**: PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite through a single interface
|
|
@@ -49,7 +49,7 @@ DBHub implements MCP tools for database operations:
|
|
|
49
49
|
- **[execute_sql](https://dbhub.ai/tools/execute-sql)**: Execute SQL queries with transaction support and safety controls
|
|
50
50
|
- **[search_objects](https://dbhub.ai/tools/search-objects)**: Search and explore database schemas, tables, columns, indexes, and procedures with progressive disclosure
|
|
51
51
|
- **[explain_sql](https://dbhub.ai/tools/explain-sql)** (opt-in): Show a query's execution plan without running it
|
|
52
|
-
- **[health_check](https://dbhub.ai/tools/health-check)** (opt-in
|
|
52
|
+
- **[health_check](https://dbhub.ai/tools/health-check)** (opt-in): Report connection pool state and buffer cache hit ratio
|
|
53
53
|
- **[Custom Tools](https://dbhub.ai/tools/custom-tools)**: Define reusable, parameterized SQL operations in your `dbhub.toml` configuration file
|
|
54
54
|
|
|
55
55
|
## Workbench
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
getDefaultPortForType,
|
|
9
9
|
parseConnectionInfoFromDSN,
|
|
10
10
|
stripCommentsAndStrings
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-WIQ4DWGY.js";
|
|
12
12
|
|
|
13
13
|
// src/tools/builtin-tools.ts
|
|
14
14
|
var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
|
|
@@ -805,6 +805,21 @@ function resolveAllowedHosts() {
|
|
|
805
805
|
function splitHostList(value) {
|
|
806
806
|
return value.split(",").map((h) => h.trim()).filter((h) => h.length > 0);
|
|
807
807
|
}
|
|
808
|
+
function resolveAuthTokens() {
|
|
809
|
+
const args = parseCommandLineArgs();
|
|
810
|
+
const cliValue = requireFlagValue("auth-token", args, "secret1,secret2");
|
|
811
|
+
if (cliValue !== void 0) {
|
|
812
|
+
return { tokens: splitTokenList(cliValue), source: "command line argument" };
|
|
813
|
+
}
|
|
814
|
+
const envValue = process.env.DBHUB_AUTH_TOKEN?.trim();
|
|
815
|
+
if (envValue) {
|
|
816
|
+
return { tokens: splitTokenList(envValue), source: "environment variable" };
|
|
817
|
+
}
|
|
818
|
+
return { tokens: [], source: "default" };
|
|
819
|
+
}
|
|
820
|
+
function splitTokenList(value) {
|
|
821
|
+
return value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
822
|
+
}
|
|
808
823
|
function redactDSN(dsn) {
|
|
809
824
|
try {
|
|
810
825
|
const url = new URL(dsn);
|
|
@@ -2492,6 +2507,7 @@ export {
|
|
|
2492
2507
|
resolvePort,
|
|
2493
2508
|
resolveHost,
|
|
2494
2509
|
resolveAllowedHosts,
|
|
2510
|
+
resolveAuthTokens,
|
|
2495
2511
|
resolveSourceConfigs,
|
|
2496
2512
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
2497
2513
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-FU2ZJE4E.js";
|
|
5
5
|
import {
|
|
6
6
|
obfuscateDSNPassword
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-WIQ4DWGY.js";
|
|
8
8
|
|
|
9
9
|
// src/connectors/mysql-family-health-check.ts
|
|
10
10
|
async function getMySQLFamilyHealthCheck(query) {
|
|
@@ -111,49 +111,36 @@ function isMultiStatementResult(results) {
|
|
|
111
111
|
const firstElement = results[0];
|
|
112
112
|
return isMetadataObject(firstElement) || Array.isArray(firstElement);
|
|
113
113
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
const allRows = [];
|
|
119
|
-
for (const result of results) {
|
|
114
|
+
function buildResultSetsFromMultiStatement(results, statementTexts) {
|
|
115
|
+
const sql = statementTexts?.length === results.length ? statementTexts : void 0;
|
|
116
|
+
return results.map((result, index) => {
|
|
120
117
|
if (Array.isArray(result)) {
|
|
121
|
-
|
|
118
|
+
return { sql: sql?.[index], rows: result, rowCount: result.length };
|
|
122
119
|
}
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
if (isMetadataObject(result)) {
|
|
121
|
+
return { sql: sql?.[index], rows: [], rowCount: result.affectedRows || 0 };
|
|
122
|
+
}
|
|
123
|
+
return { sql: sql?.[index], rows: [], rowCount: 0 };
|
|
124
|
+
});
|
|
125
125
|
}
|
|
126
|
-
function
|
|
126
|
+
function parseQueryResultSets(results, statementTexts) {
|
|
127
|
+
const singleStatementSql = statementTexts?.length === 1 ? statementTexts[0] : void 0;
|
|
127
128
|
if (isMetadataObject(results)) {
|
|
128
|
-
return results.affectedRows || 0;
|
|
129
|
+
return [{ sql: singleStatementSql, rows: [], rowCount: results.affectedRows || 0 }];
|
|
129
130
|
}
|
|
130
131
|
if (!Array.isArray(results)) {
|
|
131
|
-
return 0;
|
|
132
|
+
return [{ sql: singleStatementSql, rows: [], rowCount: 0 }];
|
|
132
133
|
}
|
|
133
134
|
if (isMultiStatementResult(results)) {
|
|
134
|
-
|
|
135
|
-
for (const result of results) {
|
|
136
|
-
if (isMetadataObject(result)) {
|
|
137
|
-
totalAffected += result.affectedRows || 0;
|
|
138
|
-
} else if (Array.isArray(result)) {
|
|
139
|
-
totalAffected += result.length;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return totalAffected;
|
|
135
|
+
return buildResultSetsFromMultiStatement(results, statementTexts);
|
|
143
136
|
}
|
|
144
|
-
return results.length;
|
|
145
|
-
}
|
|
146
|
-
function parseQueryResults(results) {
|
|
147
|
-
if (!Array.isArray(results)) {
|
|
148
|
-
return [];
|
|
149
|
-
}
|
|
150
|
-
if (isMultiStatementResult(results)) {
|
|
151
|
-
return extractRowsFromMultiStatement(results);
|
|
152
|
-
}
|
|
153
|
-
return results;
|
|
137
|
+
return [{ sql: singleStatementSql, rows: results, rowCount: results.length }];
|
|
154
138
|
}
|
|
155
139
|
|
|
156
140
|
// src/utils/readonly-transaction.ts
|
|
141
|
+
function isClientSideTimeout(error) {
|
|
142
|
+
return typeof error === "object" && error !== null && error.code === "PROTOCOL_SEQUENCE_TIMEOUT";
|
|
143
|
+
}
|
|
157
144
|
async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransaction, execute) {
|
|
158
145
|
if (!readonly) {
|
|
159
146
|
return execute();
|
|
@@ -166,9 +153,11 @@ async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransacti
|
|
|
166
153
|
await conn.query(supportsReadOnlyTransaction ? "COMMIT" : "ROLLBACK");
|
|
167
154
|
return result;
|
|
168
155
|
} catch (error) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
156
|
+
if (!isClientSideTimeout(error)) {
|
|
157
|
+
try {
|
|
158
|
+
await conn.query("ROLLBACK");
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
172
161
|
}
|
|
173
162
|
throw error;
|
|
174
163
|
}
|
|
@@ -183,8 +172,8 @@ export {
|
|
|
183
172
|
getMySQLFamilyHealthCheck,
|
|
184
173
|
MissingDatabaseError,
|
|
185
174
|
requireDatabaseInDSN,
|
|
186
|
-
|
|
187
|
-
|
|
175
|
+
parseQueryResultSets,
|
|
176
|
+
isClientSideTimeout,
|
|
188
177
|
withReadOnlyTransaction,
|
|
189
178
|
isTiDBVersion
|
|
190
179
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
blankCommentsAndStrings,
|
|
2
3
|
stripCommentsAndStrings
|
|
3
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WIQ4DWGY.js";
|
|
4
5
|
|
|
5
6
|
// src/utils/sql-row-limiter.ts
|
|
6
7
|
var SQLRowLimiter = class {
|
|
@@ -70,17 +71,77 @@ var SQLRowLimiter = class {
|
|
|
70
71
|
LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
71
72
|
}
|
|
72
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Scan blanked (comment/string-free, length-preserving) SQL for parenthesis
|
|
76
|
+
* depth, invoking onMatch for every regex hit at depth 0 (i.e. not nested
|
|
77
|
+
* inside a subquery, function call, or window OVER clause).
|
|
78
|
+
*/
|
|
79
|
+
static scanTopLevel(blankedSQL, regex, onMatch) {
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let m;
|
|
82
|
+
while ((m = regex.exec(blankedSQL)) !== null) {
|
|
83
|
+
const token = m[0];
|
|
84
|
+
if (token === "(") {
|
|
85
|
+
depth++;
|
|
86
|
+
} else if (token === ")") {
|
|
87
|
+
depth--;
|
|
88
|
+
} else if (depth === 0) {
|
|
89
|
+
onMatch(m);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if a SQL statement combines multiple SELECTs with a set operator
|
|
95
|
+
* (UNION [ALL], INTERSECT, EXCEPT) at the top level — i.e. not nested
|
|
96
|
+
* inside a subquery already wrapped in parentheses. Strips comments and
|
|
97
|
+
* string literals first to avoid false positives.
|
|
98
|
+
*/
|
|
99
|
+
static hasSetOperator(sql) {
|
|
100
|
+
const blankedSQL = blankCommentsAndStrings(sql, "sqlserver");
|
|
101
|
+
let found = false;
|
|
102
|
+
this.scanTopLevel(blankedSQL, /\(|\)|\bunion\b|\bintersect\b|\bexcept\b/gi, () => {
|
|
103
|
+
found = true;
|
|
104
|
+
});
|
|
105
|
+
return found;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Find the start index of a top-level trailing ORDER BY clause (not one
|
|
109
|
+
* nested inside a subquery or a window function's OVER (...) clause).
|
|
110
|
+
* Returns -1 if none exists. Operates on the original (non-blanked) SQL
|
|
111
|
+
* length, since blankCommentsAndStrings preserves length/position.
|
|
112
|
+
*/
|
|
113
|
+
static findTopLevelOrderByIndex(sql) {
|
|
114
|
+
const blankedSQL = blankCommentsAndStrings(sql, "sqlserver");
|
|
115
|
+
let lastIndex = -1;
|
|
116
|
+
this.scanTopLevel(blankedSQL, /\(|\)|\border\s+by\b/gi, (m) => {
|
|
117
|
+
lastIndex = m.index;
|
|
118
|
+
});
|
|
119
|
+
return lastIndex;
|
|
120
|
+
}
|
|
73
121
|
/**
|
|
74
122
|
* Add or modify TOP clause in a SQL statement (SQL Server)
|
|
75
123
|
*/
|
|
76
124
|
static applyTopToQuery(sql, maxRows) {
|
|
125
|
+
if (this.hasSetOperator(sql)) {
|
|
126
|
+
const trimmed = sql.trim();
|
|
127
|
+
const hasSemicolon = trimmed.endsWith(";");
|
|
128
|
+
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
129
|
+
const orderByIndex = this.findTopLevelOrderByIndex(sqlWithoutSemicolon);
|
|
130
|
+
if (orderByIndex !== -1) {
|
|
131
|
+
const innerSql = sqlWithoutSemicolon.slice(0, orderByIndex).trimEnd();
|
|
132
|
+
const orderByClause = sqlWithoutSemicolon.slice(orderByIndex).trim();
|
|
133
|
+
return `SELECT TOP ${maxRows} * FROM (${innerSql}
|
|
134
|
+
) AS subq ${orderByClause}${hasSemicolon ? ";" : ""}`;
|
|
135
|
+
}
|
|
136
|
+
return `SELECT TOP ${maxRows} * FROM (${sqlWithoutSemicolon}
|
|
137
|
+
) AS subq${hasSemicolon ? ";" : ""}`;
|
|
138
|
+
}
|
|
77
139
|
const existingTop = this.extractTopValue(sql);
|
|
78
140
|
if (existingTop !== null) {
|
|
79
141
|
const effectiveTop = Math.min(existingTop, maxRows);
|
|
80
142
|
return sql.replace(/\bselect\s+top\s+\d+/i, `SELECT TOP ${effectiveTop}`);
|
|
81
|
-
} else {
|
|
82
|
-
return sql.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
|
|
83
143
|
}
|
|
144
|
+
return sql.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
|
|
84
145
|
}
|
|
85
146
|
/**
|
|
86
147
|
* Check if a LIMIT clause uses a parameter placeholder (not a literal number).
|
|
@@ -462,6 +462,21 @@ function stripCommentsAndStrings(sql, dialect) {
|
|
|
462
462
|
}
|
|
463
463
|
return parts.join("");
|
|
464
464
|
}
|
|
465
|
+
function blankCommentsAndStrings(sql, dialect) {
|
|
466
|
+
const scanToken = getScanner(dialect);
|
|
467
|
+
let result = "";
|
|
468
|
+
let i = 0;
|
|
469
|
+
while (i < sql.length) {
|
|
470
|
+
const token = scanToken(sql, i);
|
|
471
|
+
if (token.type === TokenType.Plain) {
|
|
472
|
+
result += sql[i];
|
|
473
|
+
} else {
|
|
474
|
+
result += " ".repeat(token.end - i);
|
|
475
|
+
}
|
|
476
|
+
i = token.end;
|
|
477
|
+
}
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
465
480
|
function splitSQLStatements(sql, dialect) {
|
|
466
481
|
const scanToken = getScanner(dialect);
|
|
467
482
|
const statements = [];
|
|
@@ -495,5 +510,6 @@ export {
|
|
|
495
510
|
getDatabaseTypeFromDSN,
|
|
496
511
|
getDefaultPortForType,
|
|
497
512
|
stripCommentsAndStrings,
|
|
513
|
+
blankCommentsAndStrings,
|
|
498
514
|
splitSQLStatements
|
|
499
515
|
};
|
package/dist/index.js
CHANGED
|
@@ -12,19 +12,20 @@ import {
|
|
|
12
12
|
loadTomlConfig,
|
|
13
13
|
mapArgumentsToArray,
|
|
14
14
|
resolveAllowedHosts,
|
|
15
|
+
resolveAuthTokens,
|
|
15
16
|
resolveHost,
|
|
16
17
|
resolvePort,
|
|
17
18
|
resolveSourceConfigs,
|
|
18
19
|
resolveTomlConfigPath,
|
|
19
20
|
resolveTransport
|
|
20
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-CUBIR72I.js";
|
|
21
22
|
import {
|
|
22
23
|
allowedKeywords,
|
|
23
24
|
getFirstKeyword,
|
|
24
25
|
hasEscapeHatchFunction,
|
|
25
26
|
isReadOnlySQL,
|
|
26
27
|
sqlServerDynamicSqlPattern
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-PBN2UXB5.js";
|
|
28
29
|
import {
|
|
29
30
|
loadConnectors
|
|
30
31
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -38,7 +39,7 @@ import {
|
|
|
38
39
|
parseConnectionInfoFromDSN,
|
|
39
40
|
splitSQLStatements,
|
|
40
41
|
stripCommentsAndStrings
|
|
41
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-WIQ4DWGY.js";
|
|
42
43
|
|
|
43
44
|
// src/server.ts
|
|
44
45
|
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
|
|
@@ -219,6 +220,13 @@ function normalizeSourceId(id) {
|
|
|
219
220
|
function getEffectiveSourceId(sourceId) {
|
|
220
221
|
return sourceId || "default";
|
|
221
222
|
}
|
|
223
|
+
function toStatementsPayload(resultSets) {
|
|
224
|
+
return resultSets.map((set) => ({
|
|
225
|
+
sql: set.sql,
|
|
226
|
+
rows: set.rows,
|
|
227
|
+
count: set.rowCount
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
222
230
|
function resolveTrackedToolName(sourceId, baseName) {
|
|
223
231
|
const isSingleSource = ConnectorManager.getAvailableSourceIds().length === 1;
|
|
224
232
|
if (isSingleSource) {
|
|
@@ -288,8 +296,7 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
288
296
|
};
|
|
289
297
|
result = await connector.executeSQL(sql, executeOptions);
|
|
290
298
|
const responseData = {
|
|
291
|
-
|
|
292
|
-
count: result.rowCount,
|
|
299
|
+
statements: toStatementsPayload(result.resultSets),
|
|
293
300
|
source_id: effectiveSourceId,
|
|
294
301
|
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
295
302
|
};
|
|
@@ -348,8 +355,9 @@ async function getTableRowCount(connector, tableName, schemaName) {
|
|
|
348
355
|
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
349
356
|
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
350
357
|
const result = await connector.executeSQL(countQuery, { maxRows: 1, readonly: true });
|
|
351
|
-
|
|
352
|
-
|
|
358
|
+
const rows = result.resultSets[0]?.rows;
|
|
359
|
+
if (rows && rows.length > 0) {
|
|
360
|
+
return Number(rows[0].count || rows[0].COUNT || 0);
|
|
353
361
|
}
|
|
354
362
|
} catch (error) {
|
|
355
363
|
return null;
|
|
@@ -852,8 +860,8 @@ function createExplainSqlToolHandler(sourceId) {
|
|
|
852
860
|
const explainStatement = buildExplainStatement(connector.id, sql);
|
|
853
861
|
result = await connector.executeSQL(explainStatement, { readonly: true });
|
|
854
862
|
const responseData = {
|
|
855
|
-
rows: result.rows,
|
|
856
|
-
count: result.rowCount,
|
|
863
|
+
rows: result.resultSets[0].rows,
|
|
864
|
+
count: result.resultSets[0].rowCount,
|
|
857
865
|
source_id: effectiveSourceId,
|
|
858
866
|
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
859
867
|
};
|
|
@@ -1036,7 +1044,7 @@ function getExplainSqlMetadata(sourceId) {
|
|
|
1036
1044
|
"explain_sql",
|
|
1037
1045
|
"Explain Query Plan"
|
|
1038
1046
|
);
|
|
1039
|
-
const description = isSingleSource ? `${userDescPrefix}Show the execution plan for a SQL statement on the ${dbType} database without running it
|
|
1047
|
+
const description = isSingleSource ? `${userDescPrefix}Show the execution plan for a SQL statement on the ${dbType} database without running it (always read-only).` : `${userDescPrefix}Show the execution plan for a SQL statement on the '${sourceId}' ${dbType} database without running it (always read-only).`;
|
|
1040
1048
|
const annotations = {
|
|
1041
1049
|
title,
|
|
1042
1050
|
readOnlyHint: true,
|
|
@@ -1057,7 +1065,7 @@ function getHealthCheckMetadata(sourceId) {
|
|
|
1057
1065
|
"health_check",
|
|
1058
1066
|
"Health Check"
|
|
1059
1067
|
);
|
|
1060
|
-
const description = isSingleSource ? `${userDescPrefix}Report
|
|
1068
|
+
const description = isSingleSource ? `${userDescPrefix}Report connection pool and buffer cache health metrics for the ${dbType} database (read-only).` : `${userDescPrefix}Report connection pool and buffer cache health metrics for the '${sourceId}' ${dbType} database (read-only).`;
|
|
1061
1069
|
const annotations = {
|
|
1062
1070
|
title,
|
|
1063
1071
|
readOnlyHint: true,
|
|
@@ -1107,13 +1115,13 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
1107
1115
|
name: "object_type",
|
|
1108
1116
|
type: "string",
|
|
1109
1117
|
required: true,
|
|
1110
|
-
description: "Object type to search
|
|
1118
|
+
description: "Object type to search"
|
|
1111
1119
|
},
|
|
1112
1120
|
{
|
|
1113
1121
|
name: "pattern",
|
|
1114
1122
|
type: "string",
|
|
1115
1123
|
required: false,
|
|
1116
|
-
description: "LIKE pattern (% = any chars, _ = one char)
|
|
1124
|
+
description: "LIKE pattern (% = any chars, _ = one char)"
|
|
1117
1125
|
},
|
|
1118
1126
|
{
|
|
1119
1127
|
name: "schema",
|
|
@@ -1137,7 +1145,7 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
1137
1145
|
name: "limit",
|
|
1138
1146
|
type: "integer",
|
|
1139
1147
|
required: false,
|
|
1140
|
-
description: "Max results
|
|
1148
|
+
description: "Max results"
|
|
1141
1149
|
}
|
|
1142
1150
|
],
|
|
1143
1151
|
readonly: true
|
|
@@ -1267,8 +1275,7 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1267
1275
|
paramValues
|
|
1268
1276
|
);
|
|
1269
1277
|
const responseData = {
|
|
1270
|
-
|
|
1271
|
-
count: result.rowCount,
|
|
1278
|
+
statements: toStatementsPayload(result.resultSets),
|
|
1272
1279
|
source_id: toolConfig.source
|
|
1273
1280
|
};
|
|
1274
1281
|
return createToolSuccessResponse(responseData);
|
|
@@ -1822,6 +1829,32 @@ function validateOrigin(originHeader, hostHeader, allowedHosts) {
|
|
|
1822
1829
|
return { ok: true };
|
|
1823
1830
|
}
|
|
1824
1831
|
|
|
1832
|
+
// src/utils/auth-token.ts
|
|
1833
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1834
|
+
var BEARER_PREFIX = "Bearer ";
|
|
1835
|
+
function constantTimeEqual(a, b) {
|
|
1836
|
+
const bufA = Buffer.from(a);
|
|
1837
|
+
const bufB = Buffer.from(b);
|
|
1838
|
+
if (bufA.length !== bufB.length) return false;
|
|
1839
|
+
return timingSafeEqual(bufA, bufB);
|
|
1840
|
+
}
|
|
1841
|
+
function validateAuthToken(authorizationHeader, tokens) {
|
|
1842
|
+
if (tokens.length === 0) return { ok: true };
|
|
1843
|
+
if (!authorizationHeader || !authorizationHeader.startsWith(BEARER_PREFIX)) {
|
|
1844
|
+
return {
|
|
1845
|
+
ok: false,
|
|
1846
|
+
status: 401,
|
|
1847
|
+
message: "Missing or malformed Authorization header. Expected: Bearer <token>"
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
const presented = authorizationHeader.slice(BEARER_PREFIX.length);
|
|
1851
|
+
const matches = tokens.some((token) => constantTimeEqual(presented, token));
|
|
1852
|
+
if (!matches) {
|
|
1853
|
+
return { ok: false, status: 401, message: "Invalid bearer token" };
|
|
1854
|
+
}
|
|
1855
|
+
return { ok: true };
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1825
1858
|
// src/server.ts
|
|
1826
1859
|
var __filename = fileURLToPath(import.meta.url);
|
|
1827
1860
|
var __dirname = path.dirname(__filename);
|
|
@@ -1876,7 +1909,7 @@ See documentation for more details on configuring database connections.
|
|
|
1876
1909
|
const sources = sourceConfigsData.sources;
|
|
1877
1910
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1878
1911
|
await connectorManager.connectWithSources(sources);
|
|
1879
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1912
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-XMEJF7WW.js");
|
|
1880
1913
|
initializeToolRegistry2({
|
|
1881
1914
|
sources: sourceConfigsData.sources,
|
|
1882
1915
|
tools: sourceConfigsData.tools
|
|
@@ -1906,6 +1939,7 @@ See documentation for more details on configuring database connections.
|
|
|
1906
1939
|
const port = transportData.type === "http" ? resolvePort().port : null;
|
|
1907
1940
|
const host = transportData.type === "http" ? resolveHost().host : null;
|
|
1908
1941
|
const allowedHosts = transportData.type === "http" ? buildAllowedHosts(resolveAllowedHosts().hosts, host ?? void 0, getSelfHosts()) : /* @__PURE__ */ new Set();
|
|
1942
|
+
const { tokens: authTokens, source: authTokenSource } = resolveAuthTokens();
|
|
1909
1943
|
const activeModes = [];
|
|
1910
1944
|
const modeDescriptions = [];
|
|
1911
1945
|
const isDemo = isDemoMode();
|
|
@@ -1940,18 +1974,26 @@ See documentation for more details on configuring database connections.
|
|
|
1940
1974
|
}
|
|
1941
1975
|
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
1942
1976
|
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1943
|
-
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
|
|
1977
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
|
|
1944
1978
|
res.header("Access-Control-Allow-Credentials", "true");
|
|
1945
1979
|
if (req.method === "OPTIONS") {
|
|
1946
1980
|
return res.sendStatus(200);
|
|
1947
1981
|
}
|
|
1948
1982
|
next();
|
|
1949
1983
|
});
|
|
1950
|
-
const frontendPath = path.join(__dirname, "public");
|
|
1951
|
-
app.use(express.static(frontendPath));
|
|
1952
1984
|
app.get("/healthz", (req, res) => {
|
|
1953
1985
|
res.status(200).send("OK");
|
|
1954
1986
|
});
|
|
1987
|
+
app.use((req, res, next) => {
|
|
1988
|
+
const result = validateAuthToken(req.headers.authorization, authTokens);
|
|
1989
|
+
if (!result.ok) {
|
|
1990
|
+
res.header("WWW-Authenticate", "Bearer");
|
|
1991
|
+
return res.status(result.status).json({ error: "Unauthorized", message: result.message });
|
|
1992
|
+
}
|
|
1993
|
+
next();
|
|
1994
|
+
});
|
|
1995
|
+
const frontendPath = path.join(__dirname, "public");
|
|
1996
|
+
app.use(express.static(frontendPath));
|
|
1955
1997
|
app.get("/api/sources", listSources);
|
|
1956
1998
|
app.get("/api/sources/:sourceId", getSource);
|
|
1957
1999
|
app.get("/api/requests", listRequests);
|
|
@@ -1983,6 +2025,11 @@ See documentation for more details on configuring database connections.
|
|
|
1983
2025
|
} else {
|
|
1984
2026
|
console.error(`Allowed hosts: ${[...allowedHosts].join(", ")} (set --allowed-hosts to serve other hostnames)`);
|
|
1985
2027
|
}
|
|
2028
|
+
if (authTokens.length > 0) {
|
|
2029
|
+
console.error(`Auth: bearer token required (${authTokens.length} token(s) configured via ${authTokenSource})`);
|
|
2030
|
+
} else {
|
|
2031
|
+
console.error("Auth: disabled (set --auth-token or DBHUB_AUTH_TOKEN to require a bearer token)");
|
|
2032
|
+
}
|
|
1986
2033
|
if (process.env.NODE_ENV === "development") {
|
|
1987
2034
|
console.error("Development mode detected!");
|
|
1988
2035
|
console.error(" Workbench dev server (with HMR): http://localhost:5173");
|
|
@@ -2017,11 +2064,11 @@ See documentation for more details on configuring database connections.
|
|
|
2017
2064
|
|
|
2018
2065
|
// src/index.ts
|
|
2019
2066
|
var connectorModules = [
|
|
2020
|
-
{ load: () => import("./postgres-
|
|
2021
|
-
{ load: () => import("./sqlserver-
|
|
2022
|
-
{ load: () => import("./sqlite-
|
|
2023
|
-
{ load: () => import("./mysql-
|
|
2024
|
-
{ load: () => import("./mariadb-
|
|
2067
|
+
{ load: () => import("./postgres-6XY2GPQV.js"), name: "PostgreSQL", driver: "pg" },
|
|
2068
|
+
{ load: () => import("./sqlserver-IIGRXGYK.js"), name: "SQL Server", driver: "mssql" },
|
|
2069
|
+
{ load: () => import("./sqlite-CZE6Z36Y.js"), name: "SQLite", driver: "node:sqlite" },
|
|
2070
|
+
{ load: () => import("./mysql-RLDQI3SJ.js"), name: "MySQL", driver: "mysql2" },
|
|
2071
|
+
{ load: () => import("./mariadb-E33EXGJZ.js"), name: "MariaDB", driver: "mariadb" }
|
|
2025
2072
|
];
|
|
2026
2073
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
2027
2074
|
console.error("Fatal error:", error);
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MissingDatabaseError,
|
|
3
|
-
extractAffectedRows,
|
|
4
3
|
getMySQLFamilyHealthCheck,
|
|
5
4
|
isTiDBVersion,
|
|
6
|
-
|
|
5
|
+
parseQueryResultSets,
|
|
7
6
|
requireDatabaseInDSN,
|
|
8
7
|
withReadOnlyTransaction
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QEFMUQJ2.js";
|
|
10
9
|
import "./chunk-FU2ZJE4E.js";
|
|
11
10
|
import {
|
|
12
11
|
quoteIdentifier
|
|
@@ -14,13 +13,13 @@ import {
|
|
|
14
13
|
import {
|
|
15
14
|
SQLRowLimiter,
|
|
16
15
|
closeQuietly
|
|
17
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-RSB7DPO2.js";
|
|
18
17
|
import {
|
|
19
18
|
ConnectorRegistry,
|
|
20
19
|
SafeURL,
|
|
21
20
|
obfuscateDSNPassword,
|
|
22
21
|
splitSQLStatements
|
|
23
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-WIQ4DWGY.js";
|
|
24
23
|
|
|
25
24
|
// src/connectors/mariadb/index.ts
|
|
26
25
|
import * as mariadb from "mariadb";
|
|
@@ -505,9 +504,9 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
505
504
|
options.readonly,
|
|
506
505
|
this.supportsReadOnlyTransaction,
|
|
507
506
|
async () => {
|
|
507
|
+
const statements = splitSQLStatements(sql, "mariadb");
|
|
508
508
|
let processedSQL = sql;
|
|
509
509
|
if (options.maxRows) {
|
|
510
|
-
const statements = splitSQLStatements(sql, "mariadb");
|
|
511
510
|
const processedStatements = statements.map(
|
|
512
511
|
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
513
512
|
);
|
|
@@ -522,9 +521,8 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
522
521
|
} else {
|
|
523
522
|
results = await conn.query(processedSQL);
|
|
524
523
|
}
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
return { rows, rowCount };
|
|
524
|
+
const resultSets = parseQueryResultSets(results, statements);
|
|
525
|
+
return { resultSets };
|
|
528
526
|
}
|
|
529
527
|
);
|
|
530
528
|
} finally {
|