@bytebase/dbhub 0.11.10 → 0.13.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 +38 -29
- package/dist/builtin-tools-SVENYBIA.js +10 -0
- package/dist/chunk-D23WQQY7.js +13 -0
- package/dist/chunk-VWZF5OAJ.js +112 -0
- package/dist/chunk-WSLDVMBA.js +1773 -0
- package/dist/custom-tool-registry-EW3KOBGC.js +7 -0
- package/dist/index.js +1205 -2094
- package/dist/public/assets/index-gVrYRID4.css +1 -0
- package/dist/public/assets/index-hd88eD9m.js +51 -0
- package/dist/public/index.html +2 -2
- package/dist/registry-GJGPWR3I.js +11 -0
- package/package.json +3 -3
- package/dist/public/assets/index--LC9Foha.css +0 -1
- package/dist/public/assets/index-BZTaHJm6.js +0 -51
package/dist/index.js
CHANGED
|
@@ -1,205 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getToolRegistry
|
|
4
|
+
} from "./chunk-VWZF5OAJ.js";
|
|
5
|
+
import {
|
|
6
|
+
ConnectorManager,
|
|
7
|
+
ConnectorRegistry,
|
|
8
|
+
SafeURL,
|
|
9
|
+
buildDSNFromSource,
|
|
10
|
+
customToolRegistry,
|
|
11
|
+
getDatabaseTypeFromDSN,
|
|
12
|
+
getDefaultPortForType,
|
|
13
|
+
isDemoMode,
|
|
14
|
+
mapArgumentsToArray,
|
|
15
|
+
obfuscateDSNPassword,
|
|
16
|
+
parseConnectionInfoFromDSN,
|
|
17
|
+
redactDSN,
|
|
18
|
+
resolvePort,
|
|
19
|
+
resolveSourceConfigs,
|
|
20
|
+
resolveTransport,
|
|
21
|
+
stripCommentsAndStrings
|
|
22
|
+
} from "./chunk-WSLDVMBA.js";
|
|
23
|
+
import {
|
|
24
|
+
BUILTIN_TOOL_EXECUTE_SQL,
|
|
25
|
+
BUILTIN_TOOL_SEARCH_OBJECTS
|
|
26
|
+
} from "./chunk-D23WQQY7.js";
|
|
2
27
|
|
|
3
28
|
// src/connectors/postgres/index.ts
|
|
4
29
|
import pg from "pg";
|
|
5
30
|
|
|
6
|
-
// src/connectors/interface.ts
|
|
7
|
-
var _ConnectorRegistry = class _ConnectorRegistry {
|
|
8
|
-
/**
|
|
9
|
-
* Register a new connector
|
|
10
|
-
*/
|
|
11
|
-
static register(connector) {
|
|
12
|
-
_ConnectorRegistry.connectors.set(connector.id, connector);
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Get a connector by ID
|
|
16
|
-
*/
|
|
17
|
-
static getConnector(id) {
|
|
18
|
-
return _ConnectorRegistry.connectors.get(id) || null;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Get connector for a DSN string
|
|
22
|
-
* Tries to find a connector that can handle the given DSN format
|
|
23
|
-
*/
|
|
24
|
-
static getConnectorForDSN(dsn) {
|
|
25
|
-
for (const connector of _ConnectorRegistry.connectors.values()) {
|
|
26
|
-
if (connector.dsnParser.isValidDSN(dsn)) {
|
|
27
|
-
return connector;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Get all available connector IDs
|
|
34
|
-
*/
|
|
35
|
-
static getAvailableConnectors() {
|
|
36
|
-
return Array.from(_ConnectorRegistry.connectors.keys());
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Get sample DSN for a specific connector
|
|
40
|
-
*/
|
|
41
|
-
static getSampleDSN(connectorType) {
|
|
42
|
-
const connector = _ConnectorRegistry.getConnector(connectorType);
|
|
43
|
-
if (!connector) return null;
|
|
44
|
-
return connector.dsnParser.getSampleDSN();
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Get all available sample DSNs
|
|
48
|
-
*/
|
|
49
|
-
static getAllSampleDSNs() {
|
|
50
|
-
const samples = {};
|
|
51
|
-
for (const [id, connector] of _ConnectorRegistry.connectors.entries()) {
|
|
52
|
-
samples[id] = connector.dsnParser.getSampleDSN();
|
|
53
|
-
}
|
|
54
|
-
return samples;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
_ConnectorRegistry.connectors = /* @__PURE__ */ new Map();
|
|
58
|
-
var ConnectorRegistry = _ConnectorRegistry;
|
|
59
|
-
|
|
60
|
-
// src/utils/safe-url.ts
|
|
61
|
-
var SafeURL = class {
|
|
62
|
-
/**
|
|
63
|
-
* Parse a URL and handle special characters in passwords
|
|
64
|
-
* This is a safe alternative to the URL constructor
|
|
65
|
-
*
|
|
66
|
-
* @param urlString - The DSN string to parse
|
|
67
|
-
*/
|
|
68
|
-
constructor(urlString) {
|
|
69
|
-
this.protocol = "";
|
|
70
|
-
this.hostname = "";
|
|
71
|
-
this.port = "";
|
|
72
|
-
this.pathname = "";
|
|
73
|
-
this.username = "";
|
|
74
|
-
this.password = "";
|
|
75
|
-
this.searchParams = /* @__PURE__ */ new Map();
|
|
76
|
-
if (!urlString || urlString.trim() === "") {
|
|
77
|
-
throw new Error("URL string cannot be empty");
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
const protocolSeparator = urlString.indexOf("://");
|
|
81
|
-
if (protocolSeparator !== -1) {
|
|
82
|
-
this.protocol = urlString.substring(0, protocolSeparator + 1);
|
|
83
|
-
urlString = urlString.substring(protocolSeparator + 3);
|
|
84
|
-
} else {
|
|
85
|
-
throw new Error('Invalid URL format: missing protocol (e.g., "mysql://")');
|
|
86
|
-
}
|
|
87
|
-
const questionMarkIndex = urlString.indexOf("?");
|
|
88
|
-
let queryParams = "";
|
|
89
|
-
if (questionMarkIndex !== -1) {
|
|
90
|
-
queryParams = urlString.substring(questionMarkIndex + 1);
|
|
91
|
-
urlString = urlString.substring(0, questionMarkIndex);
|
|
92
|
-
queryParams.split("&").forEach((pair) => {
|
|
93
|
-
const parts = pair.split("=");
|
|
94
|
-
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
95
|
-
this.searchParams.set(parts[0], decodeURIComponent(parts[1]));
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
const atIndex = urlString.indexOf("@");
|
|
100
|
-
if (atIndex !== -1) {
|
|
101
|
-
const auth = urlString.substring(0, atIndex);
|
|
102
|
-
urlString = urlString.substring(atIndex + 1);
|
|
103
|
-
const colonIndex2 = auth.indexOf(":");
|
|
104
|
-
if (colonIndex2 !== -1) {
|
|
105
|
-
this.username = auth.substring(0, colonIndex2);
|
|
106
|
-
this.password = auth.substring(colonIndex2 + 1);
|
|
107
|
-
this.username = decodeURIComponent(this.username);
|
|
108
|
-
this.password = decodeURIComponent(this.password);
|
|
109
|
-
} else {
|
|
110
|
-
this.username = auth;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const pathSeparatorIndex = urlString.indexOf("/");
|
|
114
|
-
if (pathSeparatorIndex !== -1) {
|
|
115
|
-
this.pathname = urlString.substring(pathSeparatorIndex);
|
|
116
|
-
urlString = urlString.substring(0, pathSeparatorIndex);
|
|
117
|
-
}
|
|
118
|
-
const colonIndex = urlString.indexOf(":");
|
|
119
|
-
if (colonIndex !== -1) {
|
|
120
|
-
this.hostname = urlString.substring(0, colonIndex);
|
|
121
|
-
this.port = urlString.substring(colonIndex + 1);
|
|
122
|
-
} else {
|
|
123
|
-
this.hostname = urlString;
|
|
124
|
-
}
|
|
125
|
-
if (this.protocol === "") {
|
|
126
|
-
throw new Error("Invalid URL: protocol is required");
|
|
127
|
-
}
|
|
128
|
-
} catch (error) {
|
|
129
|
-
throw new Error(`Failed to parse URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Helper method to safely get a parameter from query string
|
|
134
|
-
*
|
|
135
|
-
* @param name - The parameter name to retrieve
|
|
136
|
-
* @returns The parameter value or null if not found
|
|
137
|
-
*/
|
|
138
|
-
getSearchParam(name) {
|
|
139
|
-
return this.searchParams.has(name) ? this.searchParams.get(name) : null;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Helper method to iterate over all parameters
|
|
143
|
-
*
|
|
144
|
-
* @param callback - Function to call for each parameter
|
|
145
|
-
*/
|
|
146
|
-
forEachSearchParam(callback) {
|
|
147
|
-
this.searchParams.forEach((value, key) => callback(value, key));
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
// src/utils/dsn-obfuscate.ts
|
|
152
|
-
function obfuscateDSNPassword(dsn) {
|
|
153
|
-
if (!dsn) {
|
|
154
|
-
return dsn;
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
const protocolMatch = dsn.match(/^([^:]+):/);
|
|
158
|
-
if (!protocolMatch) {
|
|
159
|
-
return dsn;
|
|
160
|
-
}
|
|
161
|
-
const protocol = protocolMatch[1];
|
|
162
|
-
if (protocol === "sqlite") {
|
|
163
|
-
return dsn;
|
|
164
|
-
}
|
|
165
|
-
const protocolPart = dsn.split("://")[1];
|
|
166
|
-
if (!protocolPart) {
|
|
167
|
-
return dsn;
|
|
168
|
-
}
|
|
169
|
-
const lastAtIndex = protocolPart.lastIndexOf("@");
|
|
170
|
-
if (lastAtIndex === -1) {
|
|
171
|
-
return dsn;
|
|
172
|
-
}
|
|
173
|
-
const credentialsPart = protocolPart.substring(0, lastAtIndex);
|
|
174
|
-
const hostPart = protocolPart.substring(lastAtIndex + 1);
|
|
175
|
-
const colonIndex = credentialsPart.indexOf(":");
|
|
176
|
-
if (colonIndex === -1) {
|
|
177
|
-
return dsn;
|
|
178
|
-
}
|
|
179
|
-
const username = credentialsPart.substring(0, colonIndex);
|
|
180
|
-
const password = credentialsPart.substring(colonIndex + 1);
|
|
181
|
-
const obfuscatedPassword = "*".repeat(Math.min(password.length, 8));
|
|
182
|
-
return `${protocol}://${username}:${obfuscatedPassword}@${hostPart}`;
|
|
183
|
-
} catch (error) {
|
|
184
|
-
return dsn;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
function getDatabaseTypeFromDSN(dsn) {
|
|
188
|
-
if (!dsn) {
|
|
189
|
-
return void 0;
|
|
190
|
-
}
|
|
191
|
-
const protocol = dsn.split(":")[0];
|
|
192
|
-
const protocolToType = {
|
|
193
|
-
"postgres": "postgres",
|
|
194
|
-
"postgresql": "postgres",
|
|
195
|
-
"mysql": "mysql",
|
|
196
|
-
"mariadb": "mariadb",
|
|
197
|
-
"sqlserver": "sqlserver",
|
|
198
|
-
"sqlite": "sqlite"
|
|
199
|
-
};
|
|
200
|
-
return protocolToType[protocol];
|
|
201
|
-
}
|
|
202
|
-
|
|
203
31
|
// src/utils/sql-row-limiter.ts
|
|
204
32
|
var SQLRowLimiter = class {
|
|
205
33
|
/**
|
|
@@ -211,34 +39,42 @@ var SQLRowLimiter = class {
|
|
|
211
39
|
return trimmed.startsWith("select");
|
|
212
40
|
}
|
|
213
41
|
/**
|
|
214
|
-
* Check if a SQL statement already has a LIMIT clause
|
|
42
|
+
* Check if a SQL statement already has a LIMIT clause.
|
|
43
|
+
* Strips comments and string literals first to avoid false positives.
|
|
215
44
|
*/
|
|
216
45
|
static hasLimitClause(sql2) {
|
|
217
|
-
const
|
|
218
|
-
|
|
46
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
47
|
+
const limitRegex = /\blimit\s+(?:\d+|\$\d+|\?|@p\d+)/i;
|
|
48
|
+
return limitRegex.test(cleanedSQL);
|
|
219
49
|
}
|
|
220
50
|
/**
|
|
221
|
-
* Check if a SQL statement already has a TOP clause (SQL Server)
|
|
51
|
+
* Check if a SQL statement already has a TOP clause (SQL Server).
|
|
52
|
+
* Strips comments and string literals first to avoid false positives.
|
|
222
53
|
*/
|
|
223
54
|
static hasTopClause(sql2) {
|
|
55
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
224
56
|
const topRegex = /\bselect\s+top\s+\d+/i;
|
|
225
|
-
return topRegex.test(
|
|
57
|
+
return topRegex.test(cleanedSQL);
|
|
226
58
|
}
|
|
227
59
|
/**
|
|
228
|
-
* Extract existing LIMIT value from SQL if present
|
|
60
|
+
* Extract existing LIMIT value from SQL if present.
|
|
61
|
+
* Strips comments and string literals first to avoid false positives.
|
|
229
62
|
*/
|
|
230
63
|
static extractLimitValue(sql2) {
|
|
231
|
-
const
|
|
64
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
65
|
+
const limitMatch = cleanedSQL.match(/\blimit\s+(\d+)/i);
|
|
232
66
|
if (limitMatch) {
|
|
233
67
|
return parseInt(limitMatch[1], 10);
|
|
234
68
|
}
|
|
235
69
|
return null;
|
|
236
70
|
}
|
|
237
71
|
/**
|
|
238
|
-
* Extract existing TOP value from SQL if present (SQL Server)
|
|
72
|
+
* Extract existing TOP value from SQL if present (SQL Server).
|
|
73
|
+
* Strips comments and string literals first to avoid false positives.
|
|
239
74
|
*/
|
|
240
75
|
static extractTopValue(sql2) {
|
|
241
|
-
const
|
|
76
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
77
|
+
const topMatch = cleanedSQL.match(/\bselect\s+top\s+(\d+)/i);
|
|
242
78
|
if (topMatch) {
|
|
243
79
|
return parseInt(topMatch[1], 10);
|
|
244
80
|
}
|
|
@@ -271,13 +107,34 @@ var SQLRowLimiter = class {
|
|
|
271
107
|
return sql2.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
|
|
272
108
|
}
|
|
273
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if a LIMIT clause uses a parameter placeholder (not a literal number).
|
|
112
|
+
* Strips comments and string literals first to avoid false positives.
|
|
113
|
+
*/
|
|
114
|
+
static hasParameterizedLimit(sql2) {
|
|
115
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
116
|
+
const parameterizedLimitRegex = /\blimit\s+(?:\$\d+|\?|@p\d+)/i;
|
|
117
|
+
return parameterizedLimitRegex.test(cleanedSQL);
|
|
118
|
+
}
|
|
274
119
|
/**
|
|
275
120
|
* Apply maxRows limit to a SELECT query only
|
|
121
|
+
*
|
|
122
|
+
* This method is used by PostgreSQL, MySQL, MariaDB, and SQLite connectors which all support
|
|
123
|
+
* the LIMIT clause syntax. SQL Server uses applyMaxRowsForSQLServer() instead with TOP syntax.
|
|
124
|
+
*
|
|
125
|
+
* For parameterized LIMIT clauses (e.g., LIMIT $1 or LIMIT ?), we wrap the query in a subquery
|
|
126
|
+
* to enforce max_rows as a hard cap, since the parameter value is not known until runtime.
|
|
276
127
|
*/
|
|
277
128
|
static applyMaxRows(sql2, maxRows) {
|
|
278
129
|
if (!maxRows || !this.isSelectQuery(sql2)) {
|
|
279
130
|
return sql2;
|
|
280
131
|
}
|
|
132
|
+
if (this.hasParameterizedLimit(sql2)) {
|
|
133
|
+
const trimmed = sql2.trim();
|
|
134
|
+
const hasSemicolon = trimmed.endsWith(";");
|
|
135
|
+
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
136
|
+
return `SELECT * FROM (${sqlWithoutSemicolon}) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
137
|
+
}
|
|
281
138
|
return this.applyLimitToQuery(sql2, maxRows);
|
|
282
139
|
}
|
|
283
140
|
/**
|
|
@@ -353,6 +210,11 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
353
210
|
this.name = "PostgreSQL";
|
|
354
211
|
this.dsnParser = new PostgresDSNParser();
|
|
355
212
|
this.pool = null;
|
|
213
|
+
// Source ID is set by ConnectorManager after cloning
|
|
214
|
+
this.sourceId = "default";
|
|
215
|
+
}
|
|
216
|
+
getId() {
|
|
217
|
+
return this.sourceId;
|
|
356
218
|
}
|
|
357
219
|
clone() {
|
|
358
220
|
return new _PostgresConnector();
|
|
@@ -360,9 +222,11 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
360
222
|
async connect(dsn, initScript, config) {
|
|
361
223
|
try {
|
|
362
224
|
const poolConfig = await this.dsnParser.parse(dsn, config);
|
|
225
|
+
if (config?.readonly) {
|
|
226
|
+
poolConfig.options = (poolConfig.options || "") + " -c default_transaction_read_only=on";
|
|
227
|
+
}
|
|
363
228
|
this.pool = new Pool(poolConfig);
|
|
364
229
|
const client = await this.pool.connect();
|
|
365
|
-
console.error("Successfully connected to PostgreSQL database");
|
|
366
230
|
client.release();
|
|
367
231
|
} catch (err) {
|
|
368
232
|
console.error("Failed to connect to PostgreSQL database:", err);
|
|
@@ -607,7 +471,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
607
471
|
client.release();
|
|
608
472
|
}
|
|
609
473
|
}
|
|
610
|
-
async executeSQL(sql2, options) {
|
|
474
|
+
async executeSQL(sql2, options, parameters) {
|
|
611
475
|
if (!this.pool) {
|
|
612
476
|
throw new Error("Not connected to database");
|
|
613
477
|
}
|
|
@@ -616,8 +480,21 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
616
480
|
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
617
481
|
if (statements.length === 1) {
|
|
618
482
|
const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
|
|
483
|
+
if (parameters && parameters.length > 0) {
|
|
484
|
+
try {
|
|
485
|
+
return await client.query(processedStatement, parameters);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error(`[PostgreSQL executeSQL] ERROR: ${error.message}`);
|
|
488
|
+
console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
|
|
489
|
+
console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
619
493
|
return await client.query(processedStatement);
|
|
620
494
|
} else {
|
|
495
|
+
if (parameters && parameters.length > 0) {
|
|
496
|
+
throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
|
|
497
|
+
}
|
|
621
498
|
let allRows = [];
|
|
622
499
|
await client.query("BEGIN");
|
|
623
500
|
try {
|
|
@@ -740,6 +617,11 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
740
617
|
this.id = "sqlserver";
|
|
741
618
|
this.name = "SQL Server";
|
|
742
619
|
this.dsnParser = new SQLServerDSNParser();
|
|
620
|
+
// Source ID is set by ConnectorManager after cloning
|
|
621
|
+
this.sourceId = "default";
|
|
622
|
+
}
|
|
623
|
+
getId() {
|
|
624
|
+
return this.sourceId;
|
|
743
625
|
}
|
|
744
626
|
clone() {
|
|
745
627
|
return new _SQLServerConnector();
|
|
@@ -982,16 +864,49 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
982
864
|
throw new Error(`Failed to get stored procedure details: ${error.message}`);
|
|
983
865
|
}
|
|
984
866
|
}
|
|
985
|
-
async executeSQL(
|
|
867
|
+
async executeSQL(sqlQuery, options, parameters) {
|
|
986
868
|
if (!this.connection) {
|
|
987
869
|
throw new Error("Not connected to SQL Server database");
|
|
988
870
|
}
|
|
989
871
|
try {
|
|
990
|
-
let processedSQL =
|
|
872
|
+
let processedSQL = sqlQuery;
|
|
991
873
|
if (options.maxRows) {
|
|
992
|
-
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(
|
|
874
|
+
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
|
|
875
|
+
}
|
|
876
|
+
const request = this.connection.request();
|
|
877
|
+
if (parameters && parameters.length > 0) {
|
|
878
|
+
parameters.forEach((param, index) => {
|
|
879
|
+
const paramName = `p${index + 1}`;
|
|
880
|
+
if (typeof param === "string") {
|
|
881
|
+
request.input(paramName, sql.VarChar, param);
|
|
882
|
+
} else if (typeof param === "number") {
|
|
883
|
+
if (Number.isInteger(param)) {
|
|
884
|
+
request.input(paramName, sql.Int, param);
|
|
885
|
+
} else {
|
|
886
|
+
request.input(paramName, sql.Float, param);
|
|
887
|
+
}
|
|
888
|
+
} else if (typeof param === "boolean") {
|
|
889
|
+
request.input(paramName, sql.Bit, param);
|
|
890
|
+
} else if (param === null || param === void 0) {
|
|
891
|
+
request.input(paramName, sql.VarChar, param);
|
|
892
|
+
} else if (Array.isArray(param)) {
|
|
893
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
894
|
+
} else {
|
|
895
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
let result;
|
|
900
|
+
try {
|
|
901
|
+
result = await request.query(processedSQL);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
if (parameters && parameters.length > 0) {
|
|
904
|
+
console.error(`[SQL Server executeSQL] ERROR: ${error.message}`);
|
|
905
|
+
console.error(`[SQL Server executeSQL] SQL: ${processedSQL}`);
|
|
906
|
+
console.error(`[SQL Server executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
907
|
+
}
|
|
908
|
+
throw error;
|
|
993
909
|
}
|
|
994
|
-
const result = await this.connection.request().query(processedSQL);
|
|
995
910
|
return {
|
|
996
911
|
rows: result.recordset || [],
|
|
997
912
|
fields: result.recordset && result.recordset.length > 0 ? Object.keys(result.recordset[0]).map((key) => ({
|
|
@@ -1009,6 +924,38 @@ ConnectorRegistry.register(sqlServerConnector);
|
|
|
1009
924
|
|
|
1010
925
|
// src/connectors/sqlite/index.ts
|
|
1011
926
|
import Database from "better-sqlite3";
|
|
927
|
+
|
|
928
|
+
// src/utils/identifier-quoter.ts
|
|
929
|
+
function quoteIdentifier(identifier, dbType) {
|
|
930
|
+
if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
|
|
931
|
+
throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
|
|
932
|
+
}
|
|
933
|
+
if (!identifier) {
|
|
934
|
+
throw new Error("Identifier cannot be empty");
|
|
935
|
+
}
|
|
936
|
+
switch (dbType) {
|
|
937
|
+
case "postgres":
|
|
938
|
+
case "sqlite":
|
|
939
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
940
|
+
case "mysql":
|
|
941
|
+
case "mariadb":
|
|
942
|
+
return `\`${identifier.replace(/`/g, "``")}\``;
|
|
943
|
+
case "sqlserver":
|
|
944
|
+
return `[${identifier.replace(/]/g, "]]")}]`;
|
|
945
|
+
default:
|
|
946
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
|
|
950
|
+
const quotedTable = quoteIdentifier(tableName, dbType);
|
|
951
|
+
if (schemaName) {
|
|
952
|
+
const quotedSchema = quoteIdentifier(schemaName, dbType);
|
|
953
|
+
return `${quotedSchema}.${quotedTable}`;
|
|
954
|
+
}
|
|
955
|
+
return quotedTable;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/connectors/sqlite/index.ts
|
|
1012
959
|
var SQLiteDSNParser = class {
|
|
1013
960
|
async parse(dsn, config) {
|
|
1014
961
|
if (!this.isValidDSN(dsn)) {
|
|
@@ -1059,8 +1006,13 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1059
1006
|
this.dsnParser = new SQLiteDSNParser();
|
|
1060
1007
|
this.db = null;
|
|
1061
1008
|
this.dbPath = ":memory:";
|
|
1009
|
+
// Default to in-memory database
|
|
1010
|
+
// Source ID is set by ConnectorManager after cloning
|
|
1011
|
+
this.sourceId = "default";
|
|
1012
|
+
}
|
|
1013
|
+
getId() {
|
|
1014
|
+
return this.sourceId;
|
|
1062
1015
|
}
|
|
1063
|
-
// Default to in-memory database
|
|
1064
1016
|
clone() {
|
|
1065
1017
|
return new _SQLiteConnector();
|
|
1066
1018
|
}
|
|
@@ -1073,11 +1025,13 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1073
1025
|
const parsedConfig = await this.dsnParser.parse(dsn, config);
|
|
1074
1026
|
this.dbPath = parsedConfig.dbPath;
|
|
1075
1027
|
try {
|
|
1076
|
-
|
|
1077
|
-
|
|
1028
|
+
const dbOptions = {};
|
|
1029
|
+
if (config?.readonly && this.dbPath !== ":memory:") {
|
|
1030
|
+
dbOptions.readonly = true;
|
|
1031
|
+
}
|
|
1032
|
+
this.db = new Database(this.dbPath, dbOptions);
|
|
1078
1033
|
if (initScript) {
|
|
1079
1034
|
this.db.exec(initScript);
|
|
1080
|
-
console.error("Successfully initialized database with script");
|
|
1081
1035
|
}
|
|
1082
1036
|
} catch (error) {
|
|
1083
1037
|
console.error("Failed to connect to SQLite database:", error);
|
|
@@ -1158,16 +1112,18 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1158
1112
|
AND tbl_name = ?
|
|
1159
1113
|
`
|
|
1160
1114
|
).all(tableName);
|
|
1161
|
-
const
|
|
1115
|
+
const quotedTableName = quoteIdentifier(tableName, "sqlite");
|
|
1116
|
+
const indexListRows = this.db.prepare(`PRAGMA index_list(${quotedTableName})`).all();
|
|
1162
1117
|
const indexUniqueMap = /* @__PURE__ */ new Map();
|
|
1163
1118
|
for (const indexListRow of indexListRows) {
|
|
1164
1119
|
indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
|
|
1165
1120
|
}
|
|
1166
|
-
const tableInfo = this.db.prepare(`PRAGMA table_info(${
|
|
1121
|
+
const tableInfo = this.db.prepare(`PRAGMA table_info(${quotedTableName})`).all();
|
|
1167
1122
|
const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
|
|
1168
1123
|
const results = [];
|
|
1169
1124
|
for (const indexInfo of indexInfoRows) {
|
|
1170
|
-
const
|
|
1125
|
+
const quotedIndexName = quoteIdentifier(indexInfo.index_name, "sqlite");
|
|
1126
|
+
const indexDetailRows = this.db.prepare(`PRAGMA index_info(${quotedIndexName})`).all();
|
|
1171
1127
|
const columnNames = indexDetailRows.map((row) => row.name);
|
|
1172
1128
|
results.push({
|
|
1173
1129
|
index_name: indexInfo.index_name,
|
|
@@ -1194,7 +1150,8 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1194
1150
|
throw new Error("Not connected to SQLite database");
|
|
1195
1151
|
}
|
|
1196
1152
|
try {
|
|
1197
|
-
const
|
|
1153
|
+
const quotedTableName = quoteIdentifier(tableName, "sqlite");
|
|
1154
|
+
const rows = this.db.prepare(`PRAGMA table_info(${quotedTableName})`).all();
|
|
1198
1155
|
const columns = rows.map((row) => ({
|
|
1199
1156
|
column_name: row.name,
|
|
1200
1157
|
data_type: row.type,
|
|
@@ -1221,7 +1178,7 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1221
1178
|
"SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
|
|
1222
1179
|
);
|
|
1223
1180
|
}
|
|
1224
|
-
async executeSQL(sql2, options) {
|
|
1181
|
+
async executeSQL(sql2, options, parameters) {
|
|
1225
1182
|
if (!this.db) {
|
|
1226
1183
|
throw new Error("Not connected to SQLite database");
|
|
1227
1184
|
}
|
|
@@ -1235,13 +1192,39 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1235
1192
|
processedStatement = SQLRowLimiter.applyMaxRows(processedStatement, options.maxRows);
|
|
1236
1193
|
}
|
|
1237
1194
|
if (isReadStatement) {
|
|
1238
|
-
|
|
1239
|
-
|
|
1195
|
+
if (parameters && parameters.length > 0) {
|
|
1196
|
+
try {
|
|
1197
|
+
const rows = this.db.prepare(processedStatement).all(...parameters);
|
|
1198
|
+
return { rows };
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
1201
|
+
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
|
|
1202
|
+
console.error(`[SQLite executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
1203
|
+
throw error;
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
const rows = this.db.prepare(processedStatement).all();
|
|
1207
|
+
return { rows };
|
|
1208
|
+
}
|
|
1240
1209
|
} else {
|
|
1241
|
-
|
|
1210
|
+
if (parameters && parameters.length > 0) {
|
|
1211
|
+
try {
|
|
1212
|
+
this.db.prepare(processedStatement).run(...parameters);
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
1215
|
+
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
|
|
1216
|
+
console.error(`[SQLite executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
1217
|
+
throw error;
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
this.db.prepare(processedStatement).run();
|
|
1221
|
+
}
|
|
1242
1222
|
return { rows: [] };
|
|
1243
1223
|
}
|
|
1244
1224
|
} else {
|
|
1225
|
+
if (parameters && parameters.length > 0) {
|
|
1226
|
+
throw new Error("Parameters are not supported for multi-statement queries in SQLite");
|
|
1227
|
+
}
|
|
1245
1228
|
const readStatements = [];
|
|
1246
1229
|
const writeStatements = [];
|
|
1247
1230
|
for (const statement of statements) {
|
|
@@ -1373,6 +1356,11 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1373
1356
|
this.name = "MySQL";
|
|
1374
1357
|
this.dsnParser = new MySQLDSNParser();
|
|
1375
1358
|
this.pool = null;
|
|
1359
|
+
// Source ID is set by ConnectorManager after cloning
|
|
1360
|
+
this.sourceId = "default";
|
|
1361
|
+
}
|
|
1362
|
+
getId() {
|
|
1363
|
+
return this.sourceId;
|
|
1376
1364
|
}
|
|
1377
1365
|
clone() {
|
|
1378
1366
|
return new _MySQLConnector();
|
|
@@ -1382,7 +1370,6 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1382
1370
|
const connectionOptions = await this.dsnParser.parse(dsn, config);
|
|
1383
1371
|
this.pool = mysql.createPool(connectionOptions);
|
|
1384
1372
|
const [rows] = await this.pool.query("SELECT 1");
|
|
1385
|
-
console.error("Successfully connected to MySQL database");
|
|
1386
1373
|
} catch (err) {
|
|
1387
1374
|
console.error("Failed to connect to MySQL database:", err);
|
|
1388
1375
|
throw err;
|
|
@@ -1664,7 +1651,7 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1664
1651
|
const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
|
|
1665
1652
|
return rows[0].DB;
|
|
1666
1653
|
}
|
|
1667
|
-
async executeSQL(sql2, options) {
|
|
1654
|
+
async executeSQL(sql2, options, parameters) {
|
|
1668
1655
|
if (!this.pool) {
|
|
1669
1656
|
throw new Error("Not connected to database");
|
|
1670
1657
|
}
|
|
@@ -1681,7 +1668,19 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1681
1668
|
processedSQL += ";";
|
|
1682
1669
|
}
|
|
1683
1670
|
}
|
|
1684
|
-
|
|
1671
|
+
let results;
|
|
1672
|
+
if (parameters && parameters.length > 0) {
|
|
1673
|
+
try {
|
|
1674
|
+
results = await conn.query(processedSQL, parameters);
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
console.error(`[MySQL executeSQL] ERROR: ${error.message}`);
|
|
1677
|
+
console.error(`[MySQL executeSQL] SQL: ${processedSQL}`);
|
|
1678
|
+
console.error(`[MySQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
1679
|
+
throw error;
|
|
1680
|
+
}
|
|
1681
|
+
} else {
|
|
1682
|
+
results = await conn.query(processedSQL);
|
|
1683
|
+
}
|
|
1685
1684
|
const [firstResult] = results;
|
|
1686
1685
|
const rows = parseQueryResults(firstResult);
|
|
1687
1686
|
return { rows };
|
|
@@ -1760,6 +1759,11 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
1760
1759
|
this.name = "MariaDB";
|
|
1761
1760
|
this.dsnParser = new MariadbDSNParser();
|
|
1762
1761
|
this.pool = null;
|
|
1762
|
+
// Source ID is set by ConnectorManager after cloning
|
|
1763
|
+
this.sourceId = "default";
|
|
1764
|
+
}
|
|
1765
|
+
getId() {
|
|
1766
|
+
return this.sourceId;
|
|
1763
1767
|
}
|
|
1764
1768
|
clone() {
|
|
1765
1769
|
return new _MariaDBConnector();
|
|
@@ -1768,9 +1772,7 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
1768
1772
|
try {
|
|
1769
1773
|
const connectionConfig = await this.dsnParser.parse(dsn, config);
|
|
1770
1774
|
this.pool = mariadb.createPool(connectionConfig);
|
|
1771
|
-
console.error("Testing connection to MariaDB...");
|
|
1772
1775
|
await this.pool.query("SELECT 1");
|
|
1773
|
-
console.error("Successfully connected to MariaDB database");
|
|
1774
1776
|
} catch (err) {
|
|
1775
1777
|
console.error("Failed to connect to MariaDB database:", err);
|
|
1776
1778
|
throw err;
|
|
@@ -2052,7 +2054,7 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
2052
2054
|
const rows = await this.pool.query("SELECT DATABASE() AS DB");
|
|
2053
2055
|
return rows[0].DB;
|
|
2054
2056
|
}
|
|
2055
|
-
async executeSQL(sql2, options) {
|
|
2057
|
+
async executeSQL(sql2, options, parameters) {
|
|
2056
2058
|
if (!this.pool) {
|
|
2057
2059
|
throw new Error("Not connected to database");
|
|
2058
2060
|
}
|
|
@@ -2069,7 +2071,19 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
2069
2071
|
processedSQL += ";";
|
|
2070
2072
|
}
|
|
2071
2073
|
}
|
|
2072
|
-
|
|
2074
|
+
let results;
|
|
2075
|
+
if (parameters && parameters.length > 0) {
|
|
2076
|
+
try {
|
|
2077
|
+
results = await conn.query(processedSQL, parameters);
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
console.error(`[MariaDB executeSQL] ERROR: ${error.message}`);
|
|
2080
|
+
console.error(`[MariaDB executeSQL] SQL: ${processedSQL}`);
|
|
2081
|
+
console.error(`[MariaDB executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
2082
|
+
throw error;
|
|
2083
|
+
}
|
|
2084
|
+
} else {
|
|
2085
|
+
results = await conn.query(processedSQL);
|
|
2086
|
+
}
|
|
2073
2087
|
const rows = parseQueryResults(results);
|
|
2074
2088
|
return { rows };
|
|
2075
2089
|
} catch (error) {
|
|
@@ -2084,1909 +2098,866 @@ var mariadbConnector = new MariaDBConnector();
|
|
|
2084
2098
|
ConnectorRegistry.register(mariadbConnector);
|
|
2085
2099
|
|
|
2086
2100
|
// src/server.ts
|
|
2087
|
-
import { McpServer
|
|
2101
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2088
2102
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2089
2103
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2090
2104
|
import express from "express";
|
|
2091
|
-
import
|
|
2092
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
2093
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2094
|
-
|
|
2095
|
-
// src/utils/ssh-tunnel.ts
|
|
2096
|
-
import { Client } from "ssh2";
|
|
2105
|
+
import path from "path";
|
|
2097
2106
|
import { readFileSync } from "fs";
|
|
2098
|
-
import {
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2107
|
+
import { fileURLToPath } from "url";
|
|
2108
|
+
|
|
2109
|
+
// src/tools/execute-sql.ts
|
|
2110
|
+
import { z } from "zod";
|
|
2111
|
+
|
|
2112
|
+
// src/utils/response-formatter.ts
|
|
2113
|
+
function bigIntReplacer(_key, value) {
|
|
2114
|
+
if (typeof value === "bigint") {
|
|
2115
|
+
return value.toString();
|
|
2105
2116
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
sshConfig.passphrase = config.passphrase;
|
|
2131
|
-
}
|
|
2132
|
-
} catch (error) {
|
|
2133
|
-
reject(new Error(`Failed to read private key file: ${error instanceof Error ? error.message : String(error)}`));
|
|
2134
|
-
return;
|
|
2135
|
-
}
|
|
2136
|
-
} else {
|
|
2137
|
-
reject(new Error("Either password or privateKey must be provided for SSH authentication"));
|
|
2138
|
-
return;
|
|
2117
|
+
return value;
|
|
2118
|
+
}
|
|
2119
|
+
function formatSuccessResponse(data, meta = {}) {
|
|
2120
|
+
return {
|
|
2121
|
+
success: true,
|
|
2122
|
+
data,
|
|
2123
|
+
...Object.keys(meta).length > 0 ? { meta } : {}
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function formatErrorResponse(error, code = "ERROR", details) {
|
|
2127
|
+
return {
|
|
2128
|
+
success: false,
|
|
2129
|
+
error,
|
|
2130
|
+
code,
|
|
2131
|
+
...details ? { details } : {}
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
function createToolErrorResponse(error, code = "ERROR", details) {
|
|
2135
|
+
return {
|
|
2136
|
+
content: [
|
|
2137
|
+
{
|
|
2138
|
+
type: "text",
|
|
2139
|
+
text: JSON.stringify(formatErrorResponse(error, code, details), bigIntReplacer, 2),
|
|
2140
|
+
mimeType: "application/json"
|
|
2139
2141
|
}
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
localPort: address.port,
|
|
2180
|
-
targetHost: options.targetHost,
|
|
2181
|
-
targetPort: options.targetPort
|
|
2182
|
-
};
|
|
2183
|
-
this.isConnected = true;
|
|
2184
|
-
console.error(`SSH tunnel established: localhost:${address.port} -> ${options.targetHost}:${options.targetPort}`);
|
|
2185
|
-
resolve(this.tunnelInfo);
|
|
2186
|
-
});
|
|
2187
|
-
this.localServer.on("error", (err) => {
|
|
2188
|
-
this.cleanup();
|
|
2189
|
-
reject(new Error(`Local server error: ${err.message}`));
|
|
2190
|
-
});
|
|
2191
|
-
});
|
|
2192
|
-
this.sshClient.connect(sshConfig);
|
|
2193
|
-
});
|
|
2142
|
+
],
|
|
2143
|
+
isError: true
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
function createToolSuccessResponse(data, meta = {}) {
|
|
2147
|
+
return {
|
|
2148
|
+
content: [
|
|
2149
|
+
{
|
|
2150
|
+
type: "text",
|
|
2151
|
+
text: JSON.stringify(formatSuccessResponse(data, meta), bigIntReplacer, 2),
|
|
2152
|
+
mimeType: "application/json"
|
|
2153
|
+
}
|
|
2154
|
+
]
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// src/utils/allowed-keywords.ts
|
|
2159
|
+
var allowedKeywords = {
|
|
2160
|
+
postgres: ["select", "with", "explain", "analyze", "show"],
|
|
2161
|
+
mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2162
|
+
mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2163
|
+
sqlite: ["select", "with", "explain", "analyze", "pragma"],
|
|
2164
|
+
sqlserver: ["select", "with", "explain", "showplan"]
|
|
2165
|
+
};
|
|
2166
|
+
function isReadOnlySQL(sql2, connectorType) {
|
|
2167
|
+
const cleanedSQL = stripCommentsAndStrings(sql2).trim().toLowerCase();
|
|
2168
|
+
if (!cleanedSQL) {
|
|
2169
|
+
return true;
|
|
2170
|
+
}
|
|
2171
|
+
const firstWord = cleanedSQL.split(/\s+/)[0];
|
|
2172
|
+
const keywordList = allowedKeywords[connectorType] || [];
|
|
2173
|
+
return keywordList.includes(firstWord);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// src/requests/store.ts
|
|
2177
|
+
var RequestStore = class {
|
|
2178
|
+
constructor() {
|
|
2179
|
+
this.store = /* @__PURE__ */ new Map();
|
|
2180
|
+
this.maxPerSource = 100;
|
|
2194
2181
|
}
|
|
2195
2182
|
/**
|
|
2196
|
-
*
|
|
2183
|
+
* Add a request to the store
|
|
2184
|
+
* Evicts oldest entry if at capacity
|
|
2197
2185
|
*/
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2186
|
+
add(request) {
|
|
2187
|
+
const requests = this.store.get(request.sourceId) ?? [];
|
|
2188
|
+
requests.push(request);
|
|
2189
|
+
if (requests.length > this.maxPerSource) {
|
|
2190
|
+
requests.shift();
|
|
2201
2191
|
}
|
|
2202
|
-
|
|
2203
|
-
this.cleanup();
|
|
2204
|
-
this.isConnected = false;
|
|
2205
|
-
console.error("SSH tunnel closed");
|
|
2206
|
-
resolve();
|
|
2207
|
-
});
|
|
2192
|
+
this.store.set(request.sourceId, requests);
|
|
2208
2193
|
}
|
|
2209
2194
|
/**
|
|
2210
|
-
*
|
|
2195
|
+
* Get requests, optionally filtered by source
|
|
2196
|
+
* Returns newest first
|
|
2211
2197
|
*/
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
this.
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
this.sshClient.end();
|
|
2219
|
-
this.sshClient = null;
|
|
2198
|
+
getAll(sourceId) {
|
|
2199
|
+
let requests;
|
|
2200
|
+
if (sourceId) {
|
|
2201
|
+
requests = [...this.store.get(sourceId) ?? []];
|
|
2202
|
+
} else {
|
|
2203
|
+
requests = Array.from(this.store.values()).flat();
|
|
2220
2204
|
}
|
|
2221
|
-
|
|
2205
|
+
return requests.sort(
|
|
2206
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
2207
|
+
);
|
|
2222
2208
|
}
|
|
2223
2209
|
/**
|
|
2224
|
-
* Get
|
|
2210
|
+
* Get total count of requests across all sources
|
|
2225
2211
|
*/
|
|
2226
|
-
|
|
2227
|
-
return this.
|
|
2212
|
+
getTotal() {
|
|
2213
|
+
return Array.from(this.store.values()).reduce((sum, arr) => sum + arr.length, 0);
|
|
2228
2214
|
}
|
|
2229
2215
|
/**
|
|
2230
|
-
*
|
|
2216
|
+
* Clear all requests (useful for testing)
|
|
2231
2217
|
*/
|
|
2232
|
-
|
|
2233
|
-
|
|
2218
|
+
clear() {
|
|
2219
|
+
this.store.clear();
|
|
2234
2220
|
}
|
|
2235
2221
|
};
|
|
2236
2222
|
|
|
2237
|
-
// src/
|
|
2238
|
-
|
|
2239
|
-
import path2 from "path";
|
|
2240
|
-
import { homedir as homedir3 } from "os";
|
|
2241
|
-
import toml from "@iarna/toml";
|
|
2223
|
+
// src/requests/index.ts
|
|
2224
|
+
var requestStore = new RequestStore();
|
|
2242
2225
|
|
|
2243
|
-
// src/
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2226
|
+
// src/utils/client-identifier.ts
|
|
2227
|
+
function getClientIdentifier(extra) {
|
|
2228
|
+
const userAgent = extra?.requestInfo?.headers?.["user-agent"];
|
|
2229
|
+
if (userAgent) {
|
|
2230
|
+
return userAgent;
|
|
2231
|
+
}
|
|
2232
|
+
return "stdio";
|
|
2233
|
+
}
|
|
2249
2234
|
|
|
2250
|
-
// src/
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
"~/.ssh/id_rsa",
|
|
2257
|
-
"~/.ssh/id_ed25519",
|
|
2258
|
-
"~/.ssh/id_ecdsa",
|
|
2259
|
-
"~/.ssh/id_dsa"
|
|
2260
|
-
];
|
|
2261
|
-
function expandTilde(filePath) {
|
|
2262
|
-
if (filePath.startsWith("~/")) {
|
|
2263
|
-
return join(homedir(), filePath.substring(2));
|
|
2264
|
-
}
|
|
2265
|
-
return filePath;
|
|
2235
|
+
// src/tools/execute-sql.ts
|
|
2236
|
+
var executeSqlSchema = {
|
|
2237
|
+
sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)")
|
|
2238
|
+
};
|
|
2239
|
+
function splitSQLStatements(sql2) {
|
|
2240
|
+
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
2266
2241
|
}
|
|
2267
|
-
function
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
} catch {
|
|
2271
|
-
return false;
|
|
2272
|
-
}
|
|
2242
|
+
function areAllStatementsReadOnly(sql2, connectorType) {
|
|
2243
|
+
const statements = splitSQLStatements(sql2);
|
|
2244
|
+
return statements.every((statement) => isReadOnlySQL(statement, connectorType));
|
|
2273
2245
|
}
|
|
2274
|
-
function
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2246
|
+
function createExecuteSqlToolHandler(sourceId) {
|
|
2247
|
+
return async (args, extra) => {
|
|
2248
|
+
const { sql: sql2 } = args;
|
|
2249
|
+
const startTime = Date.now();
|
|
2250
|
+
const effectiveSourceId = sourceId || "default";
|
|
2251
|
+
let success = true;
|
|
2252
|
+
let errorMessage;
|
|
2253
|
+
let result;
|
|
2254
|
+
try {
|
|
2255
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
2256
|
+
const actualSourceId = connector.getId();
|
|
2257
|
+
const registry = getToolRegistry();
|
|
2258
|
+
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
|
|
2259
|
+
const isReadonly = toolConfig?.readonly === true;
|
|
2260
|
+
if (isReadonly && !areAllStatementsReadOnly(sql2, connector.id)) {
|
|
2261
|
+
errorMessage = `Read-only mode is enabled for source '${actualSourceId}'. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
2262
|
+
success = false;
|
|
2263
|
+
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
2264
|
+
}
|
|
2265
|
+
const executeOptions = {
|
|
2266
|
+
readonly: isReadonly,
|
|
2267
|
+
max_rows: toolConfig?.max_rows
|
|
2268
|
+
};
|
|
2269
|
+
result = await connector.executeSQL(sql2, executeOptions);
|
|
2270
|
+
const responseData = {
|
|
2271
|
+
rows: result.rows,
|
|
2272
|
+
count: result.rows.length,
|
|
2273
|
+
source_id: effectiveSourceId
|
|
2274
|
+
};
|
|
2275
|
+
return createToolSuccessResponse(responseData);
|
|
2276
|
+
} catch (error) {
|
|
2277
|
+
success = false;
|
|
2278
|
+
errorMessage = error.message;
|
|
2279
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
2280
|
+
} finally {
|
|
2281
|
+
requestStore.add({
|
|
2282
|
+
id: crypto.randomUUID(),
|
|
2283
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2284
|
+
sourceId: effectiveSourceId,
|
|
2285
|
+
toolName: effectiveSourceId === "default" ? "execute_sql" : `execute_sql_${effectiveSourceId}`,
|
|
2286
|
+
sql: sql2,
|
|
2287
|
+
durationMs: Date.now() - startTime,
|
|
2288
|
+
client: getClientIdentifier(extra),
|
|
2289
|
+
success,
|
|
2290
|
+
error: errorMessage
|
|
2291
|
+
});
|
|
2278
2292
|
}
|
|
2279
|
-
}
|
|
2280
|
-
return void 0;
|
|
2293
|
+
};
|
|
2281
2294
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2295
|
+
|
|
2296
|
+
// src/tools/search-objects.ts
|
|
2297
|
+
import { z as z2 } from "zod";
|
|
2298
|
+
var searchDatabaseObjectsSchema = {
|
|
2299
|
+
object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Type of database object to search for"),
|
|
2300
|
+
pattern: z2.string().optional().default("%").describe("Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all)."),
|
|
2301
|
+
schema: z2.string().optional().describe("Filter results to a specific schema/database"),
|
|
2302
|
+
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Level of detail to return: names (minimal), summary (with metadata), full (complete structure)"),
|
|
2303
|
+
limit: z2.number().int().positive().max(1e3).default(100).describe("Maximum number of results to return (default: 100, max: 1000)")
|
|
2304
|
+
};
|
|
2305
|
+
function likePatternToRegex(pattern) {
|
|
2306
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
2307
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
2308
|
+
}
|
|
2309
|
+
async function getTableRowCount(connector, tableName, schemaName) {
|
|
2287
2310
|
try {
|
|
2288
|
-
const
|
|
2289
|
-
const
|
|
2290
|
-
const
|
|
2291
|
-
if (
|
|
2292
|
-
return
|
|
2293
|
-
}
|
|
2294
|
-
const sshConfig = {};
|
|
2295
|
-
if (hostConfig.HostName) {
|
|
2296
|
-
sshConfig.host = hostConfig.HostName;
|
|
2297
|
-
} else {
|
|
2298
|
-
sshConfig.host = hostAlias;
|
|
2299
|
-
}
|
|
2300
|
-
if (hostConfig.Port) {
|
|
2301
|
-
sshConfig.port = parseInt(hostConfig.Port, 10);
|
|
2302
|
-
}
|
|
2303
|
-
if (hostConfig.User) {
|
|
2304
|
-
sshConfig.username = hostConfig.User;
|
|
2305
|
-
}
|
|
2306
|
-
if (hostConfig.IdentityFile) {
|
|
2307
|
-
const identityFile = Array.isArray(hostConfig.IdentityFile) ? hostConfig.IdentityFile[0] : hostConfig.IdentityFile;
|
|
2308
|
-
const expandedPath = expandTilde(identityFile);
|
|
2309
|
-
if (fileExists(expandedPath)) {
|
|
2310
|
-
sshConfig.privateKey = expandedPath;
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
if (!sshConfig.privateKey) {
|
|
2314
|
-
const defaultKey = findDefaultSSHKey();
|
|
2315
|
-
if (defaultKey) {
|
|
2316
|
-
sshConfig.privateKey = defaultKey;
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
if (hostConfig.ProxyJump || hostConfig.ProxyCommand) {
|
|
2320
|
-
console.error("Warning: ProxyJump/ProxyCommand in SSH config is not yet supported by DBHub");
|
|
2311
|
+
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
2312
|
+
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
2313
|
+
const result = await connector.executeSQL(countQuery, { maxRows: 1 });
|
|
2314
|
+
if (result.rows && result.rows.length > 0) {
|
|
2315
|
+
return Number(result.rows[0].count || result.rows[0].COUNT || 0);
|
|
2321
2316
|
}
|
|
2322
|
-
if (!sshConfig.host || !sshConfig.username) {
|
|
2323
|
-
return null;
|
|
2324
|
-
}
|
|
2325
|
-
return sshConfig;
|
|
2326
2317
|
} catch (error) {
|
|
2327
|
-
console.error(`Error parsing SSH config: ${error instanceof Error ? error.message : String(error)}`);
|
|
2328
2318
|
return null;
|
|
2329
2319
|
}
|
|
2320
|
+
return null;
|
|
2330
2321
|
}
|
|
2331
|
-
function
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
if (
|
|
2336
|
-
return
|
|
2337
|
-
}
|
|
2338
|
-
if (/^[0-9a-fA-F:]+$/.test(host) && host.includes(":")) {
|
|
2339
|
-
return false;
|
|
2322
|
+
async function searchSchemas(connector, pattern, detailLevel, limit) {
|
|
2323
|
+
const schemas = await connector.getSchemas();
|
|
2324
|
+
const regex = likePatternToRegex(pattern);
|
|
2325
|
+
const matched = schemas.filter((schema) => regex.test(schema)).slice(0, limit);
|
|
2326
|
+
if (detailLevel === "names") {
|
|
2327
|
+
return matched.map((name) => ({ name }));
|
|
2340
2328
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
2346
|
-
var __dirname = path.dirname(__filename);
|
|
2347
|
-
function parseCommandLineArgs() {
|
|
2348
|
-
const args = process.argv.slice(2);
|
|
2349
|
-
const parsedManually = {};
|
|
2350
|
-
for (let i = 0; i < args.length; i++) {
|
|
2351
|
-
const arg = args[i];
|
|
2352
|
-
if (arg.startsWith("--")) {
|
|
2353
|
-
const [key, value] = arg.substring(2).split("=");
|
|
2354
|
-
if (value) {
|
|
2355
|
-
parsedManually[key] = value;
|
|
2356
|
-
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
2357
|
-
parsedManually[key] = args[i + 1];
|
|
2358
|
-
i++;
|
|
2359
|
-
} else {
|
|
2360
|
-
parsedManually[key] = "true";
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
return parsedManually;
|
|
2365
|
-
}
|
|
2366
|
-
function loadEnvFiles() {
|
|
2367
|
-
const isDevelopment = process.env.NODE_ENV === "development" || process.argv[1]?.includes("tsx");
|
|
2368
|
-
const envFileNames = isDevelopment ? [".env.local", ".env"] : [".env"];
|
|
2369
|
-
const envPaths = [];
|
|
2370
|
-
for (const fileName of envFileNames) {
|
|
2371
|
-
envPaths.push(
|
|
2372
|
-
fileName,
|
|
2373
|
-
// Current working directory
|
|
2374
|
-
path.join(__dirname, "..", "..", fileName),
|
|
2375
|
-
// Two levels up (src/config -> src -> root)
|
|
2376
|
-
path.join(process.cwd(), fileName)
|
|
2377
|
-
// Explicit current working directory
|
|
2378
|
-
);
|
|
2379
|
-
}
|
|
2380
|
-
for (const envPath of envPaths) {
|
|
2381
|
-
console.error(`Checking for env file: ${envPath}`);
|
|
2382
|
-
if (fs.existsSync(envPath)) {
|
|
2383
|
-
dotenv.config({ path: envPath });
|
|
2384
|
-
return path.basename(envPath);
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
return null;
|
|
2388
|
-
}
|
|
2389
|
-
function isDemoMode() {
|
|
2390
|
-
const args = parseCommandLineArgs();
|
|
2391
|
-
return args.demo === "true";
|
|
2392
|
-
}
|
|
2393
|
-
function isReadOnlyMode() {
|
|
2394
|
-
const args = parseCommandLineArgs();
|
|
2395
|
-
if (args.readonly !== void 0) {
|
|
2396
|
-
return args.readonly === "true";
|
|
2397
|
-
}
|
|
2398
|
-
if (process.env.READONLY !== void 0) {
|
|
2399
|
-
return process.env.READONLY === "true";
|
|
2400
|
-
}
|
|
2401
|
-
return false;
|
|
2402
|
-
}
|
|
2403
|
-
function buildDSNFromEnvParams() {
|
|
2404
|
-
const dbType = process.env.DB_TYPE;
|
|
2405
|
-
const dbHost = process.env.DB_HOST;
|
|
2406
|
-
const dbUser = process.env.DB_USER;
|
|
2407
|
-
const dbPassword = process.env.DB_PASSWORD;
|
|
2408
|
-
const dbName = process.env.DB_NAME;
|
|
2409
|
-
const dbPort = process.env.DB_PORT;
|
|
2410
|
-
if (dbType?.toLowerCase() === "sqlite") {
|
|
2411
|
-
if (!dbName) {
|
|
2412
|
-
return null;
|
|
2413
|
-
}
|
|
2414
|
-
} else {
|
|
2415
|
-
if (!dbType || !dbHost || !dbUser || !dbPassword || !dbName) {
|
|
2416
|
-
return null;
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
const supportedTypes = ["postgres", "postgresql", "mysql", "mariadb", "sqlserver", "sqlite"];
|
|
2420
|
-
if (!supportedTypes.includes(dbType.toLowerCase())) {
|
|
2421
|
-
throw new Error(`Unsupported DB_TYPE: ${dbType}. Supported types: ${supportedTypes.join(", ")}`);
|
|
2422
|
-
}
|
|
2423
|
-
let port = dbPort;
|
|
2424
|
-
if (!port) {
|
|
2425
|
-
switch (dbType.toLowerCase()) {
|
|
2426
|
-
case "postgres":
|
|
2427
|
-
case "postgresql":
|
|
2428
|
-
port = "5432";
|
|
2429
|
-
break;
|
|
2430
|
-
case "mysql":
|
|
2431
|
-
case "mariadb":
|
|
2432
|
-
port = "3306";
|
|
2433
|
-
break;
|
|
2434
|
-
case "sqlserver":
|
|
2435
|
-
port = "1433";
|
|
2436
|
-
break;
|
|
2437
|
-
case "sqlite":
|
|
2329
|
+
const results = await Promise.all(
|
|
2330
|
+
matched.map(async (schemaName) => {
|
|
2331
|
+
try {
|
|
2332
|
+
const tables = await connector.getTables(schemaName);
|
|
2438
2333
|
return {
|
|
2439
|
-
|
|
2440
|
-
|
|
2334
|
+
name: schemaName,
|
|
2335
|
+
table_count: tables.length
|
|
2336
|
+
};
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
return {
|
|
2339
|
+
name: schemaName,
|
|
2340
|
+
table_count: 0
|
|
2441
2341
|
};
|
|
2442
|
-
default:
|
|
2443
|
-
throw new Error(`Unknown database type for port determination: ${dbType}`);
|
|
2444
|
-
}
|
|
2445
|
-
}
|
|
2446
|
-
const user = dbUser;
|
|
2447
|
-
const password = dbPassword;
|
|
2448
|
-
const dbNameStr = dbName;
|
|
2449
|
-
const encodedUser = encodeURIComponent(user);
|
|
2450
|
-
const encodedPassword = encodeURIComponent(password);
|
|
2451
|
-
const encodedDbName = encodeURIComponent(dbNameStr);
|
|
2452
|
-
const protocol = dbType.toLowerCase() === "postgresql" ? "postgres" : dbType.toLowerCase();
|
|
2453
|
-
const dsn = `${protocol}://${encodedUser}:${encodedPassword}@${dbHost}:${port}/${encodedDbName}`;
|
|
2454
|
-
return {
|
|
2455
|
-
dsn,
|
|
2456
|
-
source: "individual environment variables"
|
|
2457
|
-
};
|
|
2458
|
-
}
|
|
2459
|
-
function resolveDSN() {
|
|
2460
|
-
const args = parseCommandLineArgs();
|
|
2461
|
-
if (isDemoMode()) {
|
|
2462
|
-
return {
|
|
2463
|
-
dsn: "sqlite:///:memory:",
|
|
2464
|
-
source: "demo mode",
|
|
2465
|
-
isDemo: true
|
|
2466
|
-
};
|
|
2467
|
-
}
|
|
2468
|
-
if (args.dsn) {
|
|
2469
|
-
return { dsn: args.dsn, source: "command line argument" };
|
|
2470
|
-
}
|
|
2471
|
-
if (process.env.DSN) {
|
|
2472
|
-
return { dsn: process.env.DSN, source: "environment variable" };
|
|
2473
|
-
}
|
|
2474
|
-
const envParamsResult = buildDSNFromEnvParams();
|
|
2475
|
-
if (envParamsResult) {
|
|
2476
|
-
return envParamsResult;
|
|
2477
|
-
}
|
|
2478
|
-
const loadedEnvFile = loadEnvFiles();
|
|
2479
|
-
if (loadedEnvFile && process.env.DSN) {
|
|
2480
|
-
return { dsn: process.env.DSN, source: `${loadedEnvFile} file` };
|
|
2481
|
-
}
|
|
2482
|
-
if (loadedEnvFile) {
|
|
2483
|
-
const envFileParamsResult = buildDSNFromEnvParams();
|
|
2484
|
-
if (envFileParamsResult) {
|
|
2485
|
-
return {
|
|
2486
|
-
dsn: envFileParamsResult.dsn,
|
|
2487
|
-
source: `${loadedEnvFile} file (individual parameters)`
|
|
2488
|
-
};
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
return null;
|
|
2492
|
-
}
|
|
2493
|
-
function resolveTransport() {
|
|
2494
|
-
const args = parseCommandLineArgs();
|
|
2495
|
-
if (args.transport) {
|
|
2496
|
-
const type = args.transport === "http" ? "http" : "stdio";
|
|
2497
|
-
return { type, source: "command line argument" };
|
|
2498
|
-
}
|
|
2499
|
-
if (process.env.TRANSPORT) {
|
|
2500
|
-
const type = process.env.TRANSPORT === "http" ? "http" : "stdio";
|
|
2501
|
-
return { type, source: "environment variable" };
|
|
2502
|
-
}
|
|
2503
|
-
return { type: "stdio", source: "default" };
|
|
2504
|
-
}
|
|
2505
|
-
function resolveMaxRows() {
|
|
2506
|
-
const args = parseCommandLineArgs();
|
|
2507
|
-
if (args["max-rows"]) {
|
|
2508
|
-
const maxRows = parseInt(args["max-rows"], 10);
|
|
2509
|
-
if (isNaN(maxRows) || maxRows <= 0) {
|
|
2510
|
-
throw new Error(`Invalid --max-rows value: ${args["max-rows"]}. Must be a positive integer.`);
|
|
2511
|
-
}
|
|
2512
|
-
return { maxRows, source: "command line argument" };
|
|
2513
|
-
}
|
|
2514
|
-
return null;
|
|
2515
|
-
}
|
|
2516
|
-
function resolvePort() {
|
|
2517
|
-
const args = parseCommandLineArgs();
|
|
2518
|
-
if (args.port) {
|
|
2519
|
-
const port = parseInt(args.port, 10);
|
|
2520
|
-
return { port, source: "command line argument" };
|
|
2521
|
-
}
|
|
2522
|
-
if (process.env.PORT) {
|
|
2523
|
-
const port = parseInt(process.env.PORT, 10);
|
|
2524
|
-
return { port, source: "environment variable" };
|
|
2525
|
-
}
|
|
2526
|
-
return { port: 8080, source: "default" };
|
|
2527
|
-
}
|
|
2528
|
-
function redactDSN(dsn) {
|
|
2529
|
-
try {
|
|
2530
|
-
const url = new URL(dsn);
|
|
2531
|
-
if (url.password) {
|
|
2532
|
-
url.password = "*******";
|
|
2533
|
-
}
|
|
2534
|
-
return url.toString();
|
|
2535
|
-
} catch (error) {
|
|
2536
|
-
return dsn.replace(/\/\/([^:]+):([^@]+)@/, "//$1:***@");
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
function resolveId() {
|
|
2540
|
-
const args = parseCommandLineArgs();
|
|
2541
|
-
if (args.id) {
|
|
2542
|
-
return { id: args.id, source: "command line argument" };
|
|
2543
|
-
}
|
|
2544
|
-
if (process.env.ID) {
|
|
2545
|
-
return { id: process.env.ID, source: "environment variable" };
|
|
2546
|
-
}
|
|
2547
|
-
return null;
|
|
2548
|
-
}
|
|
2549
|
-
function resolveSSHConfig() {
|
|
2550
|
-
const args = parseCommandLineArgs();
|
|
2551
|
-
const hasSSHArgs = args["ssh-host"] || process.env.SSH_HOST;
|
|
2552
|
-
if (!hasSSHArgs) {
|
|
2553
|
-
return null;
|
|
2554
|
-
}
|
|
2555
|
-
let config = {};
|
|
2556
|
-
let sources = [];
|
|
2557
|
-
let sshConfigHost;
|
|
2558
|
-
if (args["ssh-host"]) {
|
|
2559
|
-
sshConfigHost = args["ssh-host"];
|
|
2560
|
-
config.host = args["ssh-host"];
|
|
2561
|
-
sources.push("ssh-host from command line");
|
|
2562
|
-
} else if (process.env.SSH_HOST) {
|
|
2563
|
-
sshConfigHost = process.env.SSH_HOST;
|
|
2564
|
-
config.host = process.env.SSH_HOST;
|
|
2565
|
-
sources.push("SSH_HOST from environment");
|
|
2566
|
-
}
|
|
2567
|
-
if (sshConfigHost && looksLikeSSHAlias(sshConfigHost)) {
|
|
2568
|
-
const sshConfigPath = path.join(homedir2(), ".ssh", "config");
|
|
2569
|
-
console.error(`Attempting to parse SSH config for host '${sshConfigHost}' from: ${sshConfigPath}`);
|
|
2570
|
-
const sshConfigData = parseSSHConfig(sshConfigHost, sshConfigPath);
|
|
2571
|
-
if (sshConfigData) {
|
|
2572
|
-
config = { ...sshConfigData };
|
|
2573
|
-
sources.push(`SSH config for host '${sshConfigHost}'`);
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2576
|
-
if (args["ssh-port"]) {
|
|
2577
|
-
config.port = parseInt(args["ssh-port"], 10);
|
|
2578
|
-
sources.push("ssh-port from command line");
|
|
2579
|
-
} else if (process.env.SSH_PORT) {
|
|
2580
|
-
config.port = parseInt(process.env.SSH_PORT, 10);
|
|
2581
|
-
sources.push("SSH_PORT from environment");
|
|
2582
|
-
}
|
|
2583
|
-
if (args["ssh-user"]) {
|
|
2584
|
-
config.username = args["ssh-user"];
|
|
2585
|
-
sources.push("ssh-user from command line");
|
|
2586
|
-
} else if (process.env.SSH_USER) {
|
|
2587
|
-
config.username = process.env.SSH_USER;
|
|
2588
|
-
sources.push("SSH_USER from environment");
|
|
2589
|
-
}
|
|
2590
|
-
if (args["ssh-password"]) {
|
|
2591
|
-
config.password = args["ssh-password"];
|
|
2592
|
-
sources.push("ssh-password from command line");
|
|
2593
|
-
} else if (process.env.SSH_PASSWORD) {
|
|
2594
|
-
config.password = process.env.SSH_PASSWORD;
|
|
2595
|
-
sources.push("SSH_PASSWORD from environment");
|
|
2596
|
-
}
|
|
2597
|
-
if (args["ssh-key"]) {
|
|
2598
|
-
config.privateKey = args["ssh-key"];
|
|
2599
|
-
if (config.privateKey.startsWith("~/")) {
|
|
2600
|
-
config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
|
|
2601
|
-
}
|
|
2602
|
-
sources.push("ssh-key from command line");
|
|
2603
|
-
} else if (process.env.SSH_KEY) {
|
|
2604
|
-
config.privateKey = process.env.SSH_KEY;
|
|
2605
|
-
if (config.privateKey.startsWith("~/")) {
|
|
2606
|
-
config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
|
|
2607
|
-
}
|
|
2608
|
-
sources.push("SSH_KEY from environment");
|
|
2609
|
-
}
|
|
2610
|
-
if (args["ssh-passphrase"]) {
|
|
2611
|
-
config.passphrase = args["ssh-passphrase"];
|
|
2612
|
-
sources.push("ssh-passphrase from command line");
|
|
2613
|
-
} else if (process.env.SSH_PASSPHRASE) {
|
|
2614
|
-
config.passphrase = process.env.SSH_PASSPHRASE;
|
|
2615
|
-
sources.push("SSH_PASSPHRASE from environment");
|
|
2616
|
-
}
|
|
2617
|
-
if (!config.host || !config.username) {
|
|
2618
|
-
throw new Error("SSH tunnel configuration requires at least --ssh-host and --ssh-user");
|
|
2619
|
-
}
|
|
2620
|
-
if (!config.password && !config.privateKey) {
|
|
2621
|
-
throw new Error("SSH tunnel configuration requires either --ssh-password or --ssh-key for authentication");
|
|
2622
|
-
}
|
|
2623
|
-
return {
|
|
2624
|
-
config,
|
|
2625
|
-
source: sources.join(", ")
|
|
2626
|
-
};
|
|
2627
|
-
}
|
|
2628
|
-
async function resolveSourceConfigs() {
|
|
2629
|
-
if (!isDemoMode()) {
|
|
2630
|
-
const tomlConfig = loadTomlConfig();
|
|
2631
|
-
if (tomlConfig) {
|
|
2632
|
-
return tomlConfig;
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
const dsnResult = resolveDSN();
|
|
2636
|
-
if (dsnResult) {
|
|
2637
|
-
let dsnUrl;
|
|
2638
|
-
try {
|
|
2639
|
-
dsnUrl = new URL(dsnResult.dsn);
|
|
2640
|
-
} catch (error) {
|
|
2641
|
-
throw new Error(
|
|
2642
|
-
`Invalid DSN format: ${dsnResult.dsn}. Expected format: protocol://[user[:password]@]host[:port]/database`
|
|
2643
|
-
);
|
|
2644
|
-
}
|
|
2645
|
-
const protocol = dsnUrl.protocol.replace(":", "");
|
|
2646
|
-
let dbType;
|
|
2647
|
-
if (protocol === "postgresql" || protocol === "postgres") {
|
|
2648
|
-
dbType = "postgres";
|
|
2649
|
-
} else if (protocol === "mysql") {
|
|
2650
|
-
dbType = "mysql";
|
|
2651
|
-
} else if (protocol === "mariadb") {
|
|
2652
|
-
dbType = "mariadb";
|
|
2653
|
-
} else if (protocol === "sqlserver") {
|
|
2654
|
-
dbType = "sqlserver";
|
|
2655
|
-
} else if (protocol === "sqlite") {
|
|
2656
|
-
dbType = "sqlite";
|
|
2657
|
-
} else {
|
|
2658
|
-
throw new Error(`Unsupported database type in DSN: ${protocol}`);
|
|
2659
|
-
}
|
|
2660
|
-
const source = {
|
|
2661
|
-
id: "default",
|
|
2662
|
-
type: dbType,
|
|
2663
|
-
dsn: dsnResult.dsn
|
|
2664
|
-
};
|
|
2665
|
-
const sshResult = resolveSSHConfig();
|
|
2666
|
-
if (sshResult) {
|
|
2667
|
-
source.ssh_host = sshResult.config.host;
|
|
2668
|
-
source.ssh_port = sshResult.config.port;
|
|
2669
|
-
source.ssh_user = sshResult.config.username;
|
|
2670
|
-
source.ssh_password = sshResult.config.password;
|
|
2671
|
-
source.ssh_key = sshResult.config.privateKey;
|
|
2672
|
-
source.ssh_passphrase = sshResult.config.passphrase;
|
|
2673
|
-
}
|
|
2674
|
-
source.readonly = isReadOnlyMode();
|
|
2675
|
-
const maxRowsResult = resolveMaxRows();
|
|
2676
|
-
if (maxRowsResult) {
|
|
2677
|
-
source.max_rows = maxRowsResult.maxRows;
|
|
2678
|
-
}
|
|
2679
|
-
if (dsnResult.isDemo) {
|
|
2680
|
-
const { getSqliteInMemorySetupSql } = await import("./demo-loader-PSMTLZ2T.js");
|
|
2681
|
-
source.init_script = getSqliteInMemorySetupSql();
|
|
2682
|
-
}
|
|
2683
|
-
return {
|
|
2684
|
-
sources: [source],
|
|
2685
|
-
source: dsnResult.isDemo ? "demo mode" : dsnResult.source
|
|
2686
|
-
};
|
|
2687
|
-
}
|
|
2688
|
-
return null;
|
|
2689
|
-
}
|
|
2690
|
-
|
|
2691
|
-
// src/config/toml-loader.ts
|
|
2692
|
-
function loadTomlConfig() {
|
|
2693
|
-
const configPath = resolveTomlConfigPath();
|
|
2694
|
-
if (!configPath) {
|
|
2695
|
-
return null;
|
|
2696
|
-
}
|
|
2697
|
-
try {
|
|
2698
|
-
const fileContent = fs2.readFileSync(configPath, "utf-8");
|
|
2699
|
-
const parsedToml = toml.parse(fileContent);
|
|
2700
|
-
validateTomlConfig(parsedToml, configPath);
|
|
2701
|
-
const sources = processSourceConfigs(parsedToml.sources, configPath);
|
|
2702
|
-
return {
|
|
2703
|
-
sources,
|
|
2704
|
-
source: path2.basename(configPath)
|
|
2705
|
-
};
|
|
2706
|
-
} catch (error) {
|
|
2707
|
-
if (error instanceof Error) {
|
|
2708
|
-
throw new Error(
|
|
2709
|
-
`Failed to load TOML configuration from ${configPath}: ${error.message}`
|
|
2710
|
-
);
|
|
2711
|
-
}
|
|
2712
|
-
throw error;
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2715
|
-
function resolveTomlConfigPath() {
|
|
2716
|
-
const args = parseCommandLineArgs();
|
|
2717
|
-
if (args.config) {
|
|
2718
|
-
const configPath = expandHomeDir(args.config);
|
|
2719
|
-
if (!fs2.existsSync(configPath)) {
|
|
2720
|
-
throw new Error(
|
|
2721
|
-
`Configuration file specified by --config flag not found: ${configPath}`
|
|
2722
|
-
);
|
|
2723
|
-
}
|
|
2724
|
-
return configPath;
|
|
2725
|
-
}
|
|
2726
|
-
const defaultConfigPath = path2.join(process.cwd(), "dbhub.toml");
|
|
2727
|
-
if (fs2.existsSync(defaultConfigPath)) {
|
|
2728
|
-
return defaultConfigPath;
|
|
2729
|
-
}
|
|
2730
|
-
return null;
|
|
2731
|
-
}
|
|
2732
|
-
function validateTomlConfig(config, configPath) {
|
|
2733
|
-
if (!config.sources) {
|
|
2734
|
-
throw new Error(
|
|
2735
|
-
`Configuration file ${configPath} must contain a [[sources]] array. Example:
|
|
2736
|
-
|
|
2737
|
-
[[sources]]
|
|
2738
|
-
id = "my_db"
|
|
2739
|
-
dsn = "postgres://..."`
|
|
2740
|
-
);
|
|
2741
|
-
}
|
|
2742
|
-
if (!Array.isArray(config.sources)) {
|
|
2743
|
-
throw new Error(
|
|
2744
|
-
`Configuration file ${configPath}: 'sources' must be an array. Use [[sources]] syntax for array of tables in TOML.`
|
|
2745
|
-
);
|
|
2746
|
-
}
|
|
2747
|
-
if (config.sources.length === 0) {
|
|
2748
|
-
throw new Error(
|
|
2749
|
-
`Configuration file ${configPath}: sources array cannot be empty. Please define at least one source with [[sources]].`
|
|
2750
|
-
);
|
|
2751
|
-
}
|
|
2752
|
-
const ids = /* @__PURE__ */ new Set();
|
|
2753
|
-
const duplicates = [];
|
|
2754
|
-
for (const source of config.sources) {
|
|
2755
|
-
if (!source.id) {
|
|
2756
|
-
throw new Error(
|
|
2757
|
-
`Configuration file ${configPath}: each source must have an 'id' field. Example: [[sources]]
|
|
2758
|
-
id = "my_db"`
|
|
2759
|
-
);
|
|
2760
|
-
}
|
|
2761
|
-
if (ids.has(source.id)) {
|
|
2762
|
-
duplicates.push(source.id);
|
|
2763
|
-
} else {
|
|
2764
|
-
ids.add(source.id);
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
|
-
if (duplicates.length > 0) {
|
|
2768
|
-
throw new Error(
|
|
2769
|
-
`Configuration file ${configPath}: duplicate source IDs found: ${duplicates.join(", ")}. Each source must have a unique 'id' field.`
|
|
2770
|
-
);
|
|
2771
|
-
}
|
|
2772
|
-
for (const source of config.sources) {
|
|
2773
|
-
validateSourceConfig(source, configPath);
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
function validateSourceConfig(source, configPath) {
|
|
2777
|
-
const hasConnectionParams = source.type && (source.type === "sqlite" ? source.database : source.host);
|
|
2778
|
-
if (!source.dsn && !hasConnectionParams) {
|
|
2779
|
-
throw new Error(
|
|
2780
|
-
`Configuration file ${configPath}: source '${source.id}' must have either:
|
|
2781
|
-
- 'dsn' field (e.g., dsn = "postgres://user:pass@host:5432/dbname")
|
|
2782
|
-
- OR connection parameters (type, host, database, user, password)
|
|
2783
|
-
- For SQLite: type = "sqlite" and database path`
|
|
2784
|
-
);
|
|
2785
|
-
}
|
|
2786
|
-
if (source.type) {
|
|
2787
|
-
const validTypes = ["postgres", "mysql", "mariadb", "sqlserver", "sqlite"];
|
|
2788
|
-
if (!validTypes.includes(source.type)) {
|
|
2789
|
-
throw new Error(
|
|
2790
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid type '${source.type}'. Valid types: ${validTypes.join(", ")}`
|
|
2791
|
-
);
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
if (source.max_rows !== void 0) {
|
|
2795
|
-
if (typeof source.max_rows !== "number" || source.max_rows <= 0) {
|
|
2796
|
-
throw new Error(
|
|
2797
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid max_rows. Must be a positive integer.`
|
|
2798
|
-
);
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
if (source.connection_timeout !== void 0) {
|
|
2802
|
-
if (typeof source.connection_timeout !== "number" || source.connection_timeout <= 0) {
|
|
2803
|
-
throw new Error(
|
|
2804
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid connection_timeout. Must be a positive number (in seconds).`
|
|
2805
|
-
);
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
|
-
if (source.request_timeout !== void 0) {
|
|
2809
|
-
if (typeof source.request_timeout !== "number" || source.request_timeout <= 0) {
|
|
2810
|
-
throw new Error(
|
|
2811
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid request_timeout. Must be a positive number (in seconds).`
|
|
2812
|
-
);
|
|
2813
|
-
}
|
|
2814
|
-
}
|
|
2815
|
-
if (source.ssh_port !== void 0) {
|
|
2816
|
-
if (typeof source.ssh_port !== "number" || source.ssh_port <= 0 || source.ssh_port > 65535) {
|
|
2817
|
-
throw new Error(
|
|
2818
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid ssh_port. Must be between 1 and 65535.`
|
|
2819
|
-
);
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
|
-
function processSourceConfigs(sources, configPath) {
|
|
2824
|
-
return sources.map((source) => {
|
|
2825
|
-
const processed = { ...source };
|
|
2826
|
-
if (processed.ssh_key) {
|
|
2827
|
-
processed.ssh_key = expandHomeDir(processed.ssh_key);
|
|
2828
|
-
}
|
|
2829
|
-
if (processed.type === "sqlite" && processed.database) {
|
|
2830
|
-
processed.database = expandHomeDir(processed.database);
|
|
2831
|
-
}
|
|
2832
|
-
if (processed.dsn && processed.dsn.startsWith("sqlite:///~")) {
|
|
2833
|
-
processed.dsn = `sqlite:///${expandHomeDir(processed.dsn.substring(11))}`;
|
|
2834
|
-
}
|
|
2835
|
-
return processed;
|
|
2836
|
-
});
|
|
2837
|
-
}
|
|
2838
|
-
function expandHomeDir(filePath) {
|
|
2839
|
-
if (filePath.startsWith("~/")) {
|
|
2840
|
-
return path2.join(homedir3(), filePath.substring(2));
|
|
2841
|
-
}
|
|
2842
|
-
return filePath;
|
|
2843
|
-
}
|
|
2844
|
-
function buildDSNFromSource(source) {
|
|
2845
|
-
if (source.dsn) {
|
|
2846
|
-
return source.dsn;
|
|
2847
|
-
}
|
|
2848
|
-
if (!source.type) {
|
|
2849
|
-
throw new Error(
|
|
2850
|
-
`Source '${source.id}': 'type' field is required when 'dsn' is not provided`
|
|
2851
|
-
);
|
|
2852
|
-
}
|
|
2853
|
-
if (source.type === "sqlite") {
|
|
2854
|
-
if (!source.database) {
|
|
2855
|
-
throw new Error(
|
|
2856
|
-
`Source '${source.id}': 'database' field is required for SQLite`
|
|
2857
|
-
);
|
|
2858
|
-
}
|
|
2859
|
-
return `sqlite:///${source.database}`;
|
|
2860
|
-
}
|
|
2861
|
-
if (!source.host || !source.user || !source.password || !source.database) {
|
|
2862
|
-
throw new Error(
|
|
2863
|
-
`Source '${source.id}': missing required connection parameters. Required: type, host, user, password, database`
|
|
2864
|
-
);
|
|
2865
|
-
}
|
|
2866
|
-
const port = source.port || (source.type === "postgres" ? 5432 : source.type === "mysql" || source.type === "mariadb" ? 3306 : source.type === "sqlserver" ? 1433 : void 0);
|
|
2867
|
-
if (!port) {
|
|
2868
|
-
throw new Error(`Source '${source.id}': unable to determine port`);
|
|
2869
|
-
}
|
|
2870
|
-
const encodedUser = encodeURIComponent(source.user);
|
|
2871
|
-
const encodedPassword = encodeURIComponent(source.password);
|
|
2872
|
-
const encodedDatabase = encodeURIComponent(source.database);
|
|
2873
|
-
let dsn = `${source.type}://${encodedUser}:${encodedPassword}@${source.host}:${port}/${encodedDatabase}`;
|
|
2874
|
-
if (source.type === "sqlserver" && source.instanceName) {
|
|
2875
|
-
dsn += `?instanceName=${encodeURIComponent(source.instanceName)}`;
|
|
2876
|
-
}
|
|
2877
|
-
return dsn;
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
// src/connectors/manager.ts
|
|
2881
|
-
var managerInstance = null;
|
|
2882
|
-
var ConnectorManager = class {
|
|
2883
|
-
// Ordered list of source IDs (first is default)
|
|
2884
|
-
constructor() {
|
|
2885
|
-
// Maps for multi-source support
|
|
2886
|
-
this.connectors = /* @__PURE__ */ new Map();
|
|
2887
|
-
this.sshTunnels = /* @__PURE__ */ new Map();
|
|
2888
|
-
this.executeOptions = /* @__PURE__ */ new Map();
|
|
2889
|
-
this.sourceConfigs = /* @__PURE__ */ new Map();
|
|
2890
|
-
// Store original source configs
|
|
2891
|
-
this.sourceIds = [];
|
|
2892
|
-
if (!managerInstance) {
|
|
2893
|
-
managerInstance = this;
|
|
2894
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
/**
|
|
2897
|
-
* Initialize and connect to multiple databases using source configurations
|
|
2898
|
-
* This is the new multi-source connection method
|
|
2899
|
-
*/
|
|
2900
|
-
async connectWithSources(sources) {
|
|
2901
|
-
if (sources.length === 0) {
|
|
2902
|
-
throw new Error("No sources provided");
|
|
2903
|
-
}
|
|
2904
|
-
for (const source of sources) {
|
|
2905
|
-
await this.connectSource(source);
|
|
2906
|
-
}
|
|
2907
|
-
console.error(`Successfully connected to ${sources.length} database source(s)`);
|
|
2908
|
-
}
|
|
2909
|
-
/**
|
|
2910
|
-
* Connect to a single source (helper for connectWithSources)
|
|
2911
|
-
*/
|
|
2912
|
-
async connectSource(source) {
|
|
2913
|
-
const sourceId = source.id;
|
|
2914
|
-
console.error(`Connecting to source '${sourceId || "(default)"}' ...`);
|
|
2915
|
-
const dsn = buildDSNFromSource(source);
|
|
2916
|
-
let actualDSN = dsn;
|
|
2917
|
-
if (source.ssh_host) {
|
|
2918
|
-
if (!source.ssh_user) {
|
|
2919
|
-
throw new Error(
|
|
2920
|
-
`Source '${sourceId}': SSH tunnel requires ssh_user`
|
|
2921
|
-
);
|
|
2922
|
-
}
|
|
2923
|
-
const sshConfig = {
|
|
2924
|
-
host: source.ssh_host,
|
|
2925
|
-
port: source.ssh_port || 22,
|
|
2926
|
-
username: source.ssh_user,
|
|
2927
|
-
password: source.ssh_password,
|
|
2928
|
-
privateKey: source.ssh_key,
|
|
2929
|
-
passphrase: source.ssh_passphrase
|
|
2930
|
-
};
|
|
2931
|
-
if (!sshConfig.password && !sshConfig.privateKey) {
|
|
2932
|
-
throw new Error(
|
|
2933
|
-
`Source '${sourceId}': SSH tunnel requires either ssh_password or ssh_key`
|
|
2934
|
-
);
|
|
2935
|
-
}
|
|
2936
|
-
const url = new URL(dsn);
|
|
2937
|
-
const targetHost = url.hostname;
|
|
2938
|
-
const targetPort = parseInt(url.port) || this.getDefaultPort(dsn);
|
|
2939
|
-
const tunnel = new SSHTunnel();
|
|
2940
|
-
const tunnelInfo = await tunnel.establish(sshConfig, {
|
|
2941
|
-
targetHost,
|
|
2942
|
-
targetPort
|
|
2943
|
-
});
|
|
2944
|
-
url.hostname = "127.0.0.1";
|
|
2945
|
-
url.port = tunnelInfo.localPort.toString();
|
|
2946
|
-
actualDSN = url.toString();
|
|
2947
|
-
this.sshTunnels.set(sourceId, tunnel);
|
|
2948
|
-
console.error(
|
|
2949
|
-
` SSH tunnel established through localhost:${tunnelInfo.localPort}`
|
|
2950
|
-
);
|
|
2951
|
-
}
|
|
2952
|
-
const connectorPrototype = ConnectorRegistry.getConnectorForDSN(actualDSN);
|
|
2953
|
-
if (!connectorPrototype) {
|
|
2954
|
-
throw new Error(
|
|
2955
|
-
`Source '${sourceId}': No connector found for DSN: ${actualDSN}`
|
|
2956
|
-
);
|
|
2957
|
-
}
|
|
2958
|
-
const connector = connectorPrototype.clone();
|
|
2959
|
-
const config = {};
|
|
2960
|
-
if (source.connection_timeout !== void 0) {
|
|
2961
|
-
config.connectionTimeoutSeconds = source.connection_timeout;
|
|
2962
|
-
}
|
|
2963
|
-
if (connector.id === "sqlserver" && source.request_timeout !== void 0) {
|
|
2964
|
-
config.requestTimeoutSeconds = source.request_timeout;
|
|
2965
|
-
}
|
|
2966
|
-
await connector.connect(actualDSN, source.init_script, config);
|
|
2967
|
-
this.connectors.set(sourceId, connector);
|
|
2968
|
-
this.sourceIds.push(sourceId);
|
|
2969
|
-
this.sourceConfigs.set(sourceId, source);
|
|
2970
|
-
const options = {};
|
|
2971
|
-
if (source.max_rows !== void 0) {
|
|
2972
|
-
options.maxRows = source.max_rows;
|
|
2973
|
-
}
|
|
2974
|
-
if (source.readonly !== void 0) {
|
|
2975
|
-
options.readonly = source.readonly;
|
|
2976
|
-
}
|
|
2977
|
-
this.executeOptions.set(sourceId, options);
|
|
2978
|
-
console.error(` Connected successfully`);
|
|
2979
|
-
}
|
|
2980
|
-
/**
|
|
2981
|
-
* Close all database connections
|
|
2982
|
-
*/
|
|
2983
|
-
async disconnect() {
|
|
2984
|
-
for (const [sourceId, connector] of this.connectors.entries()) {
|
|
2985
|
-
try {
|
|
2986
|
-
await connector.disconnect();
|
|
2987
|
-
console.error(`Disconnected from source '${sourceId || "(default)"}'`);
|
|
2988
|
-
} catch (error) {
|
|
2989
|
-
console.error(`Error disconnecting from source '${sourceId}':`, error);
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
for (const [sourceId, tunnel] of this.sshTunnels.entries()) {
|
|
2993
|
-
try {
|
|
2994
|
-
await tunnel.close();
|
|
2995
|
-
} catch (error) {
|
|
2996
|
-
console.error(`Error closing SSH tunnel for source '${sourceId}':`, error);
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
this.connectors.clear();
|
|
3000
|
-
this.sshTunnels.clear();
|
|
3001
|
-
this.executeOptions.clear();
|
|
3002
|
-
this.sourceConfigs.clear();
|
|
3003
|
-
this.sourceIds = [];
|
|
3004
|
-
}
|
|
3005
|
-
/**
|
|
3006
|
-
* Get a connector by source ID
|
|
3007
|
-
* If sourceId is not provided, returns the default (first) connector
|
|
3008
|
-
*/
|
|
3009
|
-
getConnector(sourceId) {
|
|
3010
|
-
const id = sourceId || this.sourceIds[0];
|
|
3011
|
-
const connector = this.connectors.get(id);
|
|
3012
|
-
if (!connector) {
|
|
3013
|
-
if (sourceId) {
|
|
3014
|
-
throw new Error(
|
|
3015
|
-
`Source '${sourceId}' not found. Available sources: ${this.sourceIds.join(", ")}`
|
|
3016
|
-
);
|
|
3017
|
-
} else {
|
|
3018
|
-
throw new Error("No sources connected. Call connectWithSources() first.");
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
return connector;
|
|
3022
|
-
}
|
|
3023
|
-
/**
|
|
3024
|
-
* Get all available connector types
|
|
3025
|
-
*/
|
|
3026
|
-
static getAvailableConnectors() {
|
|
3027
|
-
return ConnectorRegistry.getAvailableConnectors();
|
|
3028
|
-
}
|
|
3029
|
-
/**
|
|
3030
|
-
* Get sample DSNs for all available connectors
|
|
3031
|
-
*/
|
|
3032
|
-
static getAllSampleDSNs() {
|
|
3033
|
-
return ConnectorRegistry.getAllSampleDSNs();
|
|
3034
|
-
}
|
|
3035
|
-
/**
|
|
3036
|
-
* Get the current active connector instance
|
|
3037
|
-
* This is used by resource and tool handlers
|
|
3038
|
-
* @param sourceId - Optional source ID. If not provided, returns default (first) connector
|
|
3039
|
-
*/
|
|
3040
|
-
static getCurrentConnector(sourceId) {
|
|
3041
|
-
if (!managerInstance) {
|
|
3042
|
-
throw new Error("ConnectorManager not initialized");
|
|
3043
|
-
}
|
|
3044
|
-
return managerInstance.getConnector(sourceId);
|
|
3045
|
-
}
|
|
3046
|
-
/**
|
|
3047
|
-
* Get execute options for SQL execution
|
|
3048
|
-
* @param sourceId - Optional source ID. If not provided, returns default options
|
|
3049
|
-
*/
|
|
3050
|
-
getExecuteOptions(sourceId) {
|
|
3051
|
-
const id = sourceId || this.sourceIds[0];
|
|
3052
|
-
return this.executeOptions.get(id) || {};
|
|
3053
|
-
}
|
|
3054
|
-
/**
|
|
3055
|
-
* Get the current execute options
|
|
3056
|
-
* This is used by tool handlers
|
|
3057
|
-
* @param sourceId - Optional source ID. If not provided, returns default options
|
|
3058
|
-
*/
|
|
3059
|
-
static getCurrentExecuteOptions(sourceId) {
|
|
3060
|
-
if (!managerInstance) {
|
|
3061
|
-
throw new Error("ConnectorManager not initialized");
|
|
3062
|
-
}
|
|
3063
|
-
return managerInstance.getExecuteOptions(sourceId);
|
|
3064
|
-
}
|
|
3065
|
-
/**
|
|
3066
|
-
* Get all available source IDs
|
|
3067
|
-
*/
|
|
3068
|
-
getSourceIds() {
|
|
3069
|
-
return [...this.sourceIds];
|
|
3070
|
-
}
|
|
3071
|
-
/** Get all available source IDs */
|
|
3072
|
-
static getAvailableSourceIds() {
|
|
3073
|
-
if (!managerInstance) {
|
|
3074
|
-
throw new Error("ConnectorManager not initialized");
|
|
3075
|
-
}
|
|
3076
|
-
return managerInstance.getSourceIds();
|
|
3077
|
-
}
|
|
3078
|
-
/**
|
|
3079
|
-
* Get source configuration by ID
|
|
3080
|
-
* @param sourceId - Source ID. If not provided, returns default (first) source config
|
|
3081
|
-
*/
|
|
3082
|
-
getSourceConfig(sourceId) {
|
|
3083
|
-
if (this.connectors.size === 0) {
|
|
3084
|
-
return null;
|
|
3085
|
-
}
|
|
3086
|
-
const id = sourceId || this.sourceIds[0];
|
|
3087
|
-
return this.sourceConfigs.get(id) || null;
|
|
3088
|
-
}
|
|
3089
|
-
/**
|
|
3090
|
-
* Get all source configurations
|
|
3091
|
-
*/
|
|
3092
|
-
getAllSourceConfigs() {
|
|
3093
|
-
return this.sourceIds.map((id) => this.sourceConfigs.get(id));
|
|
3094
|
-
}
|
|
3095
|
-
/**
|
|
3096
|
-
* Get source configuration by ID (static method for external access)
|
|
3097
|
-
*/
|
|
3098
|
-
static getSourceConfig(sourceId) {
|
|
3099
|
-
if (!managerInstance) {
|
|
3100
|
-
throw new Error("ConnectorManager not initialized");
|
|
3101
|
-
}
|
|
3102
|
-
return managerInstance.getSourceConfig(sourceId);
|
|
3103
|
-
}
|
|
3104
|
-
/**
|
|
3105
|
-
* Get all source configurations (static method for external access)
|
|
3106
|
-
*/
|
|
3107
|
-
static getAllSourceConfigs() {
|
|
3108
|
-
if (!managerInstance) {
|
|
3109
|
-
throw new Error("ConnectorManager not initialized");
|
|
3110
|
-
}
|
|
3111
|
-
return managerInstance.getAllSourceConfigs();
|
|
3112
|
-
}
|
|
3113
|
-
/**
|
|
3114
|
-
* Get default port for a database based on DSN protocol
|
|
3115
|
-
*/
|
|
3116
|
-
getDefaultPort(dsn) {
|
|
3117
|
-
if (dsn.startsWith("postgres://") || dsn.startsWith("postgresql://")) {
|
|
3118
|
-
return 5432;
|
|
3119
|
-
} else if (dsn.startsWith("mysql://")) {
|
|
3120
|
-
return 3306;
|
|
3121
|
-
} else if (dsn.startsWith("mariadb://")) {
|
|
3122
|
-
return 3306;
|
|
3123
|
-
} else if (dsn.startsWith("sqlserver://")) {
|
|
3124
|
-
return 1433;
|
|
3125
|
-
}
|
|
3126
|
-
return 0;
|
|
3127
|
-
}
|
|
3128
|
-
};
|
|
3129
|
-
|
|
3130
|
-
// src/resources/index.ts
|
|
3131
|
-
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3132
|
-
|
|
3133
|
-
// src/utils/response-formatter.ts
|
|
3134
|
-
function bigIntReplacer(_key, value) {
|
|
3135
|
-
if (typeof value === "bigint") {
|
|
3136
|
-
return value.toString();
|
|
3137
|
-
}
|
|
3138
|
-
return value;
|
|
3139
|
-
}
|
|
3140
|
-
function formatSuccessResponse(data, meta = {}) {
|
|
3141
|
-
return {
|
|
3142
|
-
success: true,
|
|
3143
|
-
data,
|
|
3144
|
-
...Object.keys(meta).length > 0 ? { meta } : {}
|
|
3145
|
-
};
|
|
3146
|
-
}
|
|
3147
|
-
function formatErrorResponse(error, code = "ERROR", details) {
|
|
3148
|
-
return {
|
|
3149
|
-
success: false,
|
|
3150
|
-
error,
|
|
3151
|
-
code,
|
|
3152
|
-
...details ? { details } : {}
|
|
3153
|
-
};
|
|
3154
|
-
}
|
|
3155
|
-
function createToolErrorResponse(error, code = "ERROR", details) {
|
|
3156
|
-
return {
|
|
3157
|
-
content: [
|
|
3158
|
-
{
|
|
3159
|
-
type: "text",
|
|
3160
|
-
text: JSON.stringify(formatErrorResponse(error, code, details), bigIntReplacer, 2),
|
|
3161
|
-
mimeType: "application/json"
|
|
3162
|
-
}
|
|
3163
|
-
],
|
|
3164
|
-
isError: true
|
|
3165
|
-
};
|
|
3166
|
-
}
|
|
3167
|
-
function createToolSuccessResponse(data, meta = {}) {
|
|
3168
|
-
return {
|
|
3169
|
-
content: [
|
|
3170
|
-
{
|
|
3171
|
-
type: "text",
|
|
3172
|
-
text: JSON.stringify(formatSuccessResponse(data, meta), bigIntReplacer, 2),
|
|
3173
|
-
mimeType: "application/json"
|
|
3174
|
-
}
|
|
3175
|
-
]
|
|
3176
|
-
};
|
|
3177
|
-
}
|
|
3178
|
-
function createResourceErrorResponse(uri, error, code = "ERROR", details) {
|
|
3179
|
-
return {
|
|
3180
|
-
contents: [
|
|
3181
|
-
{
|
|
3182
|
-
uri,
|
|
3183
|
-
text: JSON.stringify(formatErrorResponse(error, code, details), bigIntReplacer, 2),
|
|
3184
|
-
mimeType: "application/json"
|
|
3185
|
-
}
|
|
3186
|
-
]
|
|
3187
|
-
};
|
|
3188
|
-
}
|
|
3189
|
-
function createResourceSuccessResponse(uri, data, meta = {}) {
|
|
3190
|
-
return {
|
|
3191
|
-
contents: [
|
|
3192
|
-
{
|
|
3193
|
-
uri,
|
|
3194
|
-
text: JSON.stringify(formatSuccessResponse(data, meta), bigIntReplacer, 2),
|
|
3195
|
-
mimeType: "application/json"
|
|
3196
|
-
}
|
|
3197
|
-
]
|
|
3198
|
-
};
|
|
3199
|
-
}
|
|
3200
|
-
function formatPromptSuccessResponse(text, references = []) {
|
|
3201
|
-
return {
|
|
3202
|
-
messages: [
|
|
3203
|
-
{
|
|
3204
|
-
role: "assistant",
|
|
3205
|
-
content: {
|
|
3206
|
-
type: "text",
|
|
3207
|
-
text
|
|
3208
|
-
}
|
|
3209
|
-
}
|
|
3210
|
-
],
|
|
3211
|
-
...references.length > 0 ? { references } : {}
|
|
3212
|
-
};
|
|
3213
|
-
}
|
|
3214
|
-
function formatPromptErrorResponse(error, code = "ERROR") {
|
|
3215
|
-
return {
|
|
3216
|
-
messages: [
|
|
3217
|
-
{
|
|
3218
|
-
role: "assistant",
|
|
3219
|
-
content: {
|
|
3220
|
-
type: "text",
|
|
3221
|
-
text: `Error: ${error}`
|
|
3222
|
-
}
|
|
3223
|
-
}
|
|
3224
|
-
],
|
|
3225
|
-
error,
|
|
3226
|
-
code
|
|
3227
|
-
};
|
|
3228
|
-
}
|
|
3229
|
-
|
|
3230
|
-
// src/resources/tables.ts
|
|
3231
|
-
async function tablesResourceHandler(uri, variables, _extra) {
|
|
3232
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3233
|
-
const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
3234
|
-
try {
|
|
3235
|
-
if (schemaName) {
|
|
3236
|
-
const availableSchemas = await connector.getSchemas();
|
|
3237
|
-
if (!availableSchemas.includes(schemaName)) {
|
|
3238
|
-
return createResourceErrorResponse(
|
|
3239
|
-
uri.href,
|
|
3240
|
-
`Schema '${schemaName}' does not exist or cannot be accessed`,
|
|
3241
|
-
"SCHEMA_NOT_FOUND"
|
|
3242
|
-
);
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
const tableNames = await connector.getTables(schemaName);
|
|
3246
|
-
const responseData = {
|
|
3247
|
-
tables: tableNames,
|
|
3248
|
-
count: tableNames.length,
|
|
3249
|
-
schema: schemaName
|
|
3250
|
-
};
|
|
3251
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3252
|
-
} catch (error) {
|
|
3253
|
-
return createResourceErrorResponse(
|
|
3254
|
-
uri.href,
|
|
3255
|
-
`Error retrieving tables: ${error.message}`,
|
|
3256
|
-
"TABLES_RETRIEVAL_ERROR"
|
|
3257
|
-
);
|
|
3258
|
-
}
|
|
3259
|
-
}
|
|
3260
|
-
|
|
3261
|
-
// src/resources/schema.ts
|
|
3262
|
-
async function tableStructureResourceHandler(uri, variables, _extra) {
|
|
3263
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3264
|
-
const tableName = Array.isArray(variables.tableName) ? variables.tableName[0] : variables.tableName;
|
|
3265
|
-
const schemaName = variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
3266
|
-
try {
|
|
3267
|
-
if (schemaName) {
|
|
3268
|
-
const availableSchemas = await connector.getSchemas();
|
|
3269
|
-
if (!availableSchemas.includes(schemaName)) {
|
|
3270
|
-
return createResourceErrorResponse(
|
|
3271
|
-
uri.href,
|
|
3272
|
-
`Schema '${schemaName}' does not exist or cannot be accessed`,
|
|
3273
|
-
"SCHEMA_NOT_FOUND"
|
|
3274
|
-
);
|
|
3275
|
-
}
|
|
3276
|
-
}
|
|
3277
|
-
const tableExists = await connector.tableExists(tableName, schemaName);
|
|
3278
|
-
if (!tableExists) {
|
|
3279
|
-
const schemaInfo = schemaName ? ` in schema '${schemaName}'` : "";
|
|
3280
|
-
return createResourceErrorResponse(
|
|
3281
|
-
uri.href,
|
|
3282
|
-
`Table '${tableName}'${schemaInfo} does not exist or cannot be accessed`,
|
|
3283
|
-
"TABLE_NOT_FOUND"
|
|
3284
|
-
);
|
|
3285
|
-
}
|
|
3286
|
-
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
3287
|
-
const formattedColumns = columns.map((col) => ({
|
|
3288
|
-
name: col.column_name,
|
|
3289
|
-
type: col.data_type,
|
|
3290
|
-
nullable: col.is_nullable === "YES",
|
|
3291
|
-
default: col.column_default
|
|
3292
|
-
}));
|
|
3293
|
-
const responseData = {
|
|
3294
|
-
table: tableName,
|
|
3295
|
-
schema: schemaName,
|
|
3296
|
-
columns: formattedColumns,
|
|
3297
|
-
count: formattedColumns.length
|
|
3298
|
-
};
|
|
3299
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3300
|
-
} catch (error) {
|
|
3301
|
-
return createResourceErrorResponse(
|
|
3302
|
-
uri.href,
|
|
3303
|
-
`Error retrieving schema: ${error.message}`,
|
|
3304
|
-
"SCHEMA_RETRIEVAL_ERROR"
|
|
3305
|
-
);
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
// src/resources/schemas.ts
|
|
3310
|
-
async function schemasResourceHandler(uri, _extra) {
|
|
3311
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3312
|
-
try {
|
|
3313
|
-
const schemas = await connector.getSchemas();
|
|
3314
|
-
const responseData = {
|
|
3315
|
-
schemas,
|
|
3316
|
-
count: schemas.length
|
|
3317
|
-
};
|
|
3318
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3319
|
-
} catch (error) {
|
|
3320
|
-
return createResourceErrorResponse(
|
|
3321
|
-
uri.href,
|
|
3322
|
-
`Error retrieving database schemas: ${error.message}`,
|
|
3323
|
-
"SCHEMAS_RETRIEVAL_ERROR"
|
|
3324
|
-
);
|
|
3325
|
-
}
|
|
3326
|
-
}
|
|
3327
|
-
|
|
3328
|
-
// src/resources/indexes.ts
|
|
3329
|
-
async function indexesResourceHandler(uri, variables, _extra) {
|
|
3330
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3331
|
-
const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
3332
|
-
const tableName = variables && variables.tableName ? Array.isArray(variables.tableName) ? variables.tableName[0] : variables.tableName : void 0;
|
|
3333
|
-
if (!tableName) {
|
|
3334
|
-
return createResourceErrorResponse(uri.href, "Table name is required", "MISSING_TABLE_NAME");
|
|
3335
|
-
}
|
|
3336
|
-
try {
|
|
3337
|
-
if (schemaName) {
|
|
3338
|
-
const availableSchemas = await connector.getSchemas();
|
|
3339
|
-
if (!availableSchemas.includes(schemaName)) {
|
|
3340
|
-
return createResourceErrorResponse(
|
|
3341
|
-
uri.href,
|
|
3342
|
-
`Schema '${schemaName}' does not exist or cannot be accessed`,
|
|
3343
|
-
"SCHEMA_NOT_FOUND"
|
|
3344
|
-
);
|
|
3345
|
-
}
|
|
3346
|
-
}
|
|
3347
|
-
const tableExists = await connector.tableExists(tableName, schemaName);
|
|
3348
|
-
if (!tableExists) {
|
|
3349
|
-
return createResourceErrorResponse(
|
|
3350
|
-
uri.href,
|
|
3351
|
-
`Table '${tableName}' does not exist in schema '${schemaName || "default"}'`,
|
|
3352
|
-
"TABLE_NOT_FOUND"
|
|
3353
|
-
);
|
|
3354
|
-
}
|
|
3355
|
-
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
3356
|
-
const responseData = {
|
|
3357
|
-
table: tableName,
|
|
3358
|
-
schema: schemaName,
|
|
3359
|
-
indexes,
|
|
3360
|
-
count: indexes.length
|
|
3361
|
-
};
|
|
3362
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3363
|
-
} catch (error) {
|
|
3364
|
-
return createResourceErrorResponse(
|
|
3365
|
-
uri.href,
|
|
3366
|
-
`Error retrieving indexes: ${error.message}`,
|
|
3367
|
-
"INDEXES_RETRIEVAL_ERROR"
|
|
3368
|
-
);
|
|
3369
|
-
}
|
|
3370
|
-
}
|
|
3371
|
-
|
|
3372
|
-
// src/resources/procedures.ts
|
|
3373
|
-
async function proceduresResourceHandler(uri, variables, _extra) {
|
|
3374
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3375
|
-
const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
3376
|
-
try {
|
|
3377
|
-
if (schemaName) {
|
|
3378
|
-
const availableSchemas = await connector.getSchemas();
|
|
3379
|
-
if (!availableSchemas.includes(schemaName)) {
|
|
3380
|
-
return createResourceErrorResponse(
|
|
3381
|
-
uri.href,
|
|
3382
|
-
`Schema '${schemaName}' does not exist or cannot be accessed`,
|
|
3383
|
-
"SCHEMA_NOT_FOUND"
|
|
3384
|
-
);
|
|
3385
|
-
}
|
|
3386
|
-
}
|
|
3387
|
-
const procedureNames = await connector.getStoredProcedures(schemaName);
|
|
3388
|
-
const responseData = {
|
|
3389
|
-
procedures: procedureNames,
|
|
3390
|
-
count: procedureNames.length,
|
|
3391
|
-
schema: schemaName
|
|
3392
|
-
};
|
|
3393
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3394
|
-
} catch (error) {
|
|
3395
|
-
return createResourceErrorResponse(
|
|
3396
|
-
uri.href,
|
|
3397
|
-
`Error retrieving stored procedures: ${error.message}`,
|
|
3398
|
-
"PROCEDURES_RETRIEVAL_ERROR"
|
|
3399
|
-
);
|
|
3400
|
-
}
|
|
3401
|
-
}
|
|
3402
|
-
async function procedureDetailResourceHandler(uri, variables, _extra) {
|
|
3403
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3404
|
-
const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
3405
|
-
const procedureName = variables && variables.procedureName ? Array.isArray(variables.procedureName) ? variables.procedureName[0] : variables.procedureName : void 0;
|
|
3406
|
-
if (!procedureName) {
|
|
3407
|
-
return createResourceErrorResponse(uri.href, "Procedure name is required", "MISSING_PARAMETER");
|
|
3408
|
-
}
|
|
3409
|
-
try {
|
|
3410
|
-
if (schemaName) {
|
|
3411
|
-
const availableSchemas = await connector.getSchemas();
|
|
3412
|
-
if (!availableSchemas.includes(schemaName)) {
|
|
3413
|
-
return createResourceErrorResponse(
|
|
3414
|
-
uri.href,
|
|
3415
|
-
`Schema '${schemaName}' does not exist or cannot be accessed`,
|
|
3416
|
-
"SCHEMA_NOT_FOUND"
|
|
3417
|
-
);
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
const procedureDetails = await connector.getStoredProcedureDetail(procedureName, schemaName);
|
|
3421
|
-
const responseData = {
|
|
3422
|
-
procedureName: procedureDetails.procedure_name,
|
|
3423
|
-
procedureType: procedureDetails.procedure_type,
|
|
3424
|
-
language: procedureDetails.language,
|
|
3425
|
-
parameters: procedureDetails.parameter_list,
|
|
3426
|
-
returnType: procedureDetails.return_type,
|
|
3427
|
-
definition: procedureDetails.definition,
|
|
3428
|
-
schema: schemaName
|
|
3429
|
-
};
|
|
3430
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3431
|
-
} catch (error) {
|
|
3432
|
-
return createResourceErrorResponse(
|
|
3433
|
-
uri.href,
|
|
3434
|
-
`Error retrieving procedure details: ${error.message}`,
|
|
3435
|
-
"PROCEDURE_DETAILS_ERROR"
|
|
3436
|
-
);
|
|
3437
|
-
}
|
|
3438
|
-
}
|
|
3439
|
-
|
|
3440
|
-
// src/resources/index.ts
|
|
3441
|
-
function registerResources(server) {
|
|
3442
|
-
server.resource(
|
|
3443
|
-
"schemas",
|
|
3444
|
-
"db://schemas",
|
|
3445
|
-
{
|
|
3446
|
-
description: "List all schemas/databases available in the connected database",
|
|
3447
|
-
mimeType: "application/json"
|
|
3448
|
-
},
|
|
3449
|
-
schemasResourceHandler
|
|
3450
|
-
);
|
|
3451
|
-
server.resource(
|
|
3452
|
-
"tables_in_schema",
|
|
3453
|
-
new ResourceTemplate("db://schemas/{schemaName}/tables", { list: void 0 }),
|
|
3454
|
-
{
|
|
3455
|
-
description: "List all tables within a specific schema",
|
|
3456
|
-
mimeType: "application/json"
|
|
3457
|
-
},
|
|
3458
|
-
tablesResourceHandler
|
|
3459
|
-
);
|
|
3460
|
-
server.resource(
|
|
3461
|
-
"table_structure_in_schema",
|
|
3462
|
-
new ResourceTemplate("db://schemas/{schemaName}/tables/{tableName}", { list: void 0 }),
|
|
3463
|
-
{
|
|
3464
|
-
description: "Get detailed structure information for a specific table, including columns, data types, and constraints",
|
|
3465
|
-
mimeType: "application/json"
|
|
3466
|
-
},
|
|
3467
|
-
tableStructureResourceHandler
|
|
3468
|
-
);
|
|
3469
|
-
server.resource(
|
|
3470
|
-
"indexes_in_table",
|
|
3471
|
-
new ResourceTemplate("db://schemas/{schemaName}/tables/{tableName}/indexes", {
|
|
3472
|
-
list: void 0
|
|
3473
|
-
}),
|
|
3474
|
-
{
|
|
3475
|
-
description: "List all indexes defined on a specific table",
|
|
3476
|
-
mimeType: "application/json"
|
|
3477
|
-
},
|
|
3478
|
-
indexesResourceHandler
|
|
3479
|
-
);
|
|
3480
|
-
server.resource(
|
|
3481
|
-
"procedures_in_schema",
|
|
3482
|
-
new ResourceTemplate("db://schemas/{schemaName}/procedures", { list: void 0 }),
|
|
3483
|
-
{
|
|
3484
|
-
description: "List all stored procedures/functions in a schema (not supported by SQLite)",
|
|
3485
|
-
mimeType: "application/json"
|
|
3486
|
-
},
|
|
3487
|
-
proceduresResourceHandler
|
|
3488
|
-
);
|
|
3489
|
-
server.resource(
|
|
3490
|
-
"procedure_detail_in_schema",
|
|
3491
|
-
new ResourceTemplate("db://schemas/{schemaName}/procedures/{procedureName}", {
|
|
3492
|
-
list: void 0
|
|
3493
|
-
}),
|
|
3494
|
-
{
|
|
3495
|
-
description: "Get detailed information about a specific stored procedure, including parameters and definition (not supported by SQLite)",
|
|
3496
|
-
mimeType: "application/json"
|
|
3497
|
-
},
|
|
3498
|
-
procedureDetailResourceHandler
|
|
3499
|
-
);
|
|
3500
|
-
}
|
|
3501
|
-
|
|
3502
|
-
// src/tools/execute-sql.ts
|
|
3503
|
-
import { z } from "zod";
|
|
3504
|
-
|
|
3505
|
-
// src/utils/allowed-keywords.ts
|
|
3506
|
-
var allowedKeywords = {
|
|
3507
|
-
postgres: ["select", "with", "explain", "analyze", "show"],
|
|
3508
|
-
mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
3509
|
-
mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
3510
|
-
sqlite: ["select", "with", "explain", "analyze", "pragma"],
|
|
3511
|
-
sqlserver: ["select", "with", "explain", "showplan"]
|
|
3512
|
-
};
|
|
3513
|
-
|
|
3514
|
-
// src/tools/execute-sql.ts
|
|
3515
|
-
var executeSqlSchema = {
|
|
3516
|
-
sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)"),
|
|
3517
|
-
source_id: z.string().optional().describe("Database source ID to execute the query against (optional, defaults to first configured source)")
|
|
3518
|
-
};
|
|
3519
|
-
function splitSQLStatements(sql2) {
|
|
3520
|
-
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
3521
|
-
}
|
|
3522
|
-
function stripSQLComments(sql2) {
|
|
3523
|
-
let cleaned = sql2.split("\n").map((line) => {
|
|
3524
|
-
const commentIndex = line.indexOf("--");
|
|
3525
|
-
return commentIndex >= 0 ? line.substring(0, commentIndex) : line;
|
|
3526
|
-
}).join("\n");
|
|
3527
|
-
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
3528
|
-
return cleaned.trim();
|
|
3529
|
-
}
|
|
3530
|
-
function isReadOnlySQL(sql2, connectorType) {
|
|
3531
|
-
const cleanedSQL = stripSQLComments(sql2).toLowerCase();
|
|
3532
|
-
if (!cleanedSQL) {
|
|
3533
|
-
return true;
|
|
3534
|
-
}
|
|
3535
|
-
const firstWord = cleanedSQL.split(/\s+/)[0];
|
|
3536
|
-
const keywordList = allowedKeywords[connectorType] || allowedKeywords.default || [];
|
|
3537
|
-
return keywordList.includes(firstWord);
|
|
3538
|
-
}
|
|
3539
|
-
function areAllStatementsReadOnly(sql2, connectorType) {
|
|
3540
|
-
const statements = splitSQLStatements(sql2);
|
|
3541
|
-
return statements.every((statement) => isReadOnlySQL(statement, connectorType));
|
|
3542
|
-
}
|
|
3543
|
-
async function executeSqlToolHandler({ sql: sql2, source_id }, _extra) {
|
|
3544
|
-
try {
|
|
3545
|
-
const connector = ConnectorManager.getCurrentConnector(source_id);
|
|
3546
|
-
const executeOptions = ConnectorManager.getCurrentExecuteOptions(source_id);
|
|
3547
|
-
if (isReadOnlyMode() && !areAllStatementsReadOnly(sql2, connector.id)) {
|
|
3548
|
-
return createToolErrorResponse(
|
|
3549
|
-
`Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`,
|
|
3550
|
-
"READONLY_VIOLATION"
|
|
3551
|
-
);
|
|
3552
|
-
}
|
|
3553
|
-
const result = await connector.executeSQL(sql2, executeOptions);
|
|
3554
|
-
const responseData = {
|
|
3555
|
-
rows: result.rows,
|
|
3556
|
-
count: result.rows.length,
|
|
3557
|
-
source_id: source_id || "(default)"
|
|
3558
|
-
// Include source_id in response for clarity
|
|
3559
|
-
};
|
|
3560
|
-
return createToolSuccessResponse(responseData);
|
|
3561
|
-
} catch (error) {
|
|
3562
|
-
return createToolErrorResponse(error.message, "EXECUTION_ERROR");
|
|
3563
|
-
}
|
|
3564
|
-
}
|
|
3565
|
-
|
|
3566
|
-
// src/tools/index.ts
|
|
3567
|
-
function registerTools(server, id) {
|
|
3568
|
-
const toolName = id ? `execute_sql_${id}` : "execute_sql";
|
|
3569
|
-
server.tool(
|
|
3570
|
-
toolName,
|
|
3571
|
-
"Execute a SQL query on the current database",
|
|
3572
|
-
executeSqlSchema,
|
|
3573
|
-
executeSqlToolHandler
|
|
3574
|
-
);
|
|
3575
|
-
}
|
|
3576
|
-
|
|
3577
|
-
// src/prompts/sql-generator.ts
|
|
3578
|
-
import { z as z2 } from "zod";
|
|
3579
|
-
var sqlGeneratorSchema = {
|
|
3580
|
-
description: z2.string().describe("Natural language description of the SQL query to generate"),
|
|
3581
|
-
schema: z2.string().optional().describe("Optional database schema to use")
|
|
3582
|
-
};
|
|
3583
|
-
async function sqlGeneratorPromptHandler({
|
|
3584
|
-
description,
|
|
3585
|
-
schema
|
|
3586
|
-
}, _extra) {
|
|
3587
|
-
try {
|
|
3588
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3589
|
-
let sqlDialect;
|
|
3590
|
-
switch (connector.id) {
|
|
3591
|
-
case "postgres":
|
|
3592
|
-
sqlDialect = "postgres";
|
|
3593
|
-
break;
|
|
3594
|
-
case "sqlite":
|
|
3595
|
-
sqlDialect = "sqlite";
|
|
3596
|
-
break;
|
|
3597
|
-
case "mysql":
|
|
3598
|
-
sqlDialect = "mysql";
|
|
3599
|
-
break;
|
|
3600
|
-
case "sqlserver":
|
|
3601
|
-
sqlDialect = "mssql";
|
|
3602
|
-
break;
|
|
3603
|
-
default:
|
|
3604
|
-
sqlDialect = "ansi";
|
|
3605
|
-
}
|
|
3606
|
-
if (schema) {
|
|
3607
|
-
const availableSchemas = await connector.getSchemas();
|
|
3608
|
-
if (!availableSchemas.includes(schema)) {
|
|
3609
|
-
return formatPromptErrorResponse(
|
|
3610
|
-
`Schema '${schema}' does not exist or cannot be accessed. Available schemas: ${availableSchemas.join(", ")}`,
|
|
3611
|
-
"SCHEMA_NOT_FOUND"
|
|
3612
|
-
);
|
|
3613
2342
|
}
|
|
3614
|
-
}
|
|
2343
|
+
})
|
|
2344
|
+
);
|
|
2345
|
+
return results;
|
|
2346
|
+
}
|
|
2347
|
+
async function searchTables(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2348
|
+
const regex = likePatternToRegex(pattern);
|
|
2349
|
+
const results = [];
|
|
2350
|
+
let schemasToSearch;
|
|
2351
|
+
if (schemaFilter) {
|
|
2352
|
+
schemasToSearch = [schemaFilter];
|
|
2353
|
+
} else {
|
|
2354
|
+
schemasToSearch = await connector.getSchemas();
|
|
2355
|
+
}
|
|
2356
|
+
for (const schemaName of schemasToSearch) {
|
|
2357
|
+
if (results.length >= limit) break;
|
|
3615
2358
|
try {
|
|
3616
|
-
const tables = await connector.getTables(
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
2359
|
+
const tables = await connector.getTables(schemaName);
|
|
2360
|
+
const matched = tables.filter((table) => regex.test(table));
|
|
2361
|
+
for (const tableName of matched) {
|
|
2362
|
+
if (results.length >= limit) break;
|
|
2363
|
+
if (detailLevel === "names") {
|
|
2364
|
+
results.push({
|
|
2365
|
+
name: tableName,
|
|
2366
|
+
schema: schemaName
|
|
2367
|
+
});
|
|
2368
|
+
} else if (detailLevel === "summary") {
|
|
2369
|
+
try {
|
|
2370
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
2371
|
+
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
2372
|
+
results.push({
|
|
2373
|
+
name: tableName,
|
|
2374
|
+
schema: schemaName,
|
|
2375
|
+
column_count: columns.length,
|
|
2376
|
+
row_count: rowCount
|
|
2377
|
+
});
|
|
2378
|
+
} catch (error) {
|
|
2379
|
+
results.push({
|
|
2380
|
+
name: tableName,
|
|
2381
|
+
schema: schemaName,
|
|
2382
|
+
column_count: null,
|
|
2383
|
+
row_count: null
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
} else {
|
|
3626
2387
|
try {
|
|
3627
|
-
const columns = await connector.getTableSchema(
|
|
3628
|
-
|
|
3629
|
-
|
|
2388
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
2389
|
+
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
2390
|
+
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
2391
|
+
results.push({
|
|
2392
|
+
name: tableName,
|
|
2393
|
+
schema: schemaName,
|
|
2394
|
+
column_count: columns.length,
|
|
2395
|
+
row_count: rowCount,
|
|
3630
2396
|
columns: columns.map((col) => ({
|
|
3631
2397
|
name: col.column_name,
|
|
3632
|
-
type: col.data_type
|
|
2398
|
+
type: col.data_type,
|
|
2399
|
+
nullable: col.is_nullable === "YES",
|
|
2400
|
+
default: col.column_default
|
|
2401
|
+
})),
|
|
2402
|
+
indexes: indexes.map((idx) => ({
|
|
2403
|
+
name: idx.index_name,
|
|
2404
|
+
columns: idx.column_names,
|
|
2405
|
+
unique: idx.is_unique,
|
|
2406
|
+
primary: idx.is_primary
|
|
3633
2407
|
}))
|
|
3634
|
-
};
|
|
2408
|
+
});
|
|
3635
2409
|
} catch (error) {
|
|
3636
|
-
|
|
2410
|
+
results.push({
|
|
2411
|
+
name: tableName,
|
|
2412
|
+
schema: schemaName,
|
|
2413
|
+
error: `Unable to fetch full details: ${error.message}`
|
|
2414
|
+
});
|
|
3637
2415
|
}
|
|
3638
|
-
}
|
|
3639
|
-
);
|
|
3640
|
-
const accessibleSchemas = tableSchemas.filter((schema2) => schema2 !== null);
|
|
3641
|
-
if (accessibleSchemas.length === 0) {
|
|
3642
|
-
return formatPromptErrorResponse(
|
|
3643
|
-
`No accessible tables found. You may not have sufficient permissions to access table schemas.`,
|
|
3644
|
-
"NO_ACCESSIBLE_TABLES"
|
|
3645
|
-
);
|
|
3646
|
-
}
|
|
3647
|
-
const schemaContext = accessibleSchemas.length > 0 ? `Available tables and their columns:
|
|
3648
|
-
${accessibleSchemas.map(
|
|
3649
|
-
(schema2) => `- ${schema2.table}: ${schema2.columns.map((col) => `${col.name} (${col.type})`).join(", ")}`
|
|
3650
|
-
).join("\n")}` : "No schema information available.";
|
|
3651
|
-
const dialectExamples = {
|
|
3652
|
-
postgres: [
|
|
3653
|
-
"SELECT * FROM users WHERE created_at > NOW() - INTERVAL '1 day'",
|
|
3654
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
3655
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
3656
|
-
],
|
|
3657
|
-
sqlite: [
|
|
3658
|
-
"SELECT * FROM users WHERE created_at > datetime('now', '-1 day')",
|
|
3659
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
3660
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
3661
|
-
],
|
|
3662
|
-
mysql: [
|
|
3663
|
-
"SELECT * FROM users WHERE created_at > NOW() - INTERVAL 1 DAY",
|
|
3664
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
3665
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
3666
|
-
],
|
|
3667
|
-
mssql: [
|
|
3668
|
-
"SELECT * FROM users WHERE created_at > DATEADD(day, -1, GETDATE())",
|
|
3669
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
3670
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
3671
|
-
],
|
|
3672
|
-
ansi: [
|
|
3673
|
-
"SELECT * FROM users WHERE created_at > CURRENT_TIMESTAMP - INTERVAL '1' DAY",
|
|
3674
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
3675
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
3676
|
-
]
|
|
3677
|
-
};
|
|
3678
|
-
const schemaInfo = schema ? `in schema '${schema}'` : "across all schemas";
|
|
3679
|
-
const prompt = `
|
|
3680
|
-
Generate a ${sqlDialect} SQL query based on this description: "${description}"
|
|
3681
|
-
|
|
3682
|
-
${schemaContext}
|
|
3683
|
-
Working ${schemaInfo}
|
|
3684
|
-
|
|
3685
|
-
The query should:
|
|
3686
|
-
1. Be written for ${sqlDialect} dialect
|
|
3687
|
-
2. Use only the available tables and columns
|
|
3688
|
-
3. Prioritize readability
|
|
3689
|
-
4. Include appropriate comments
|
|
3690
|
-
5. Be compatible with ${sqlDialect} syntax
|
|
3691
|
-
`;
|
|
3692
|
-
let generatedSQL;
|
|
3693
|
-
if (description.toLowerCase().includes("count")) {
|
|
3694
|
-
const schemaPrefix = schema ? `-- Schema: ${schema}
|
|
3695
|
-
` : "";
|
|
3696
|
-
generatedSQL = `${schemaPrefix}-- Count query generated from: "${description}"
|
|
3697
|
-
SELECT COUNT(*) AS count
|
|
3698
|
-
FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name"};`;
|
|
3699
|
-
} else if (description.toLowerCase().includes("average") || description.toLowerCase().includes("avg")) {
|
|
3700
|
-
const table = accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name";
|
|
3701
|
-
const numericColumn = accessibleSchemas.length > 0 ? accessibleSchemas[0].columns.find(
|
|
3702
|
-
(col) => ["int", "numeric", "decimal", "float", "real", "double"].some(
|
|
3703
|
-
(t) => col.type.includes(t)
|
|
3704
|
-
)
|
|
3705
|
-
)?.name || "numeric_column" : "numeric_column";
|
|
3706
|
-
const schemaPrefix = schema ? `-- Schema: ${schema}
|
|
3707
|
-
` : "";
|
|
3708
|
-
generatedSQL = `${schemaPrefix}-- Average query generated from: "${description}"
|
|
3709
|
-
SELECT AVG(${numericColumn}) AS average
|
|
3710
|
-
FROM ${table};`;
|
|
3711
|
-
} else if (description.toLowerCase().includes("join")) {
|
|
3712
|
-
const schemaPrefix = schema ? `-- Schema: ${schema}
|
|
3713
|
-
` : "";
|
|
3714
|
-
generatedSQL = `${schemaPrefix}-- Join query generated from: "${description}"
|
|
3715
|
-
SELECT t1.*, t2.*
|
|
3716
|
-
FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0]?.table : "table1"} t1
|
|
3717
|
-
JOIN ${accessibleSchemas.length > 1 ? accessibleSchemas[1]?.table : "table2"} t2
|
|
3718
|
-
ON t1.id = t2.${accessibleSchemas.length > 0 ? accessibleSchemas[0]?.table : "table1"}_id;`;
|
|
3719
|
-
} else {
|
|
3720
|
-
const table = accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name";
|
|
3721
|
-
const schemaPrefix = schema ? `-- Schema: ${schema}
|
|
3722
|
-
` : "";
|
|
3723
|
-
generatedSQL = `${schemaPrefix}-- Query generated from: "${description}"
|
|
3724
|
-
SELECT *
|
|
3725
|
-
FROM ${table}
|
|
3726
|
-
LIMIT 10;`;
|
|
2416
|
+
}
|
|
3727
2417
|
}
|
|
3728
|
-
return formatPromptSuccessResponse(
|
|
3729
|
-
generatedSQL,
|
|
3730
|
-
// Add references to example queries that could help
|
|
3731
|
-
dialectExamples[sqlDialect]
|
|
3732
|
-
);
|
|
3733
2418
|
} catch (error) {
|
|
3734
|
-
|
|
3735
|
-
`Error generating SQL query schema information: ${error.message}`,
|
|
3736
|
-
"SCHEMA_RETRIEVAL_ERROR"
|
|
3737
|
-
);
|
|
2419
|
+
continue;
|
|
3738
2420
|
}
|
|
3739
|
-
} catch (error) {
|
|
3740
|
-
return formatPromptErrorResponse(
|
|
3741
|
-
`Failed to generate SQL: ${error.message}`,
|
|
3742
|
-
"SQL_GENERATION_ERROR"
|
|
3743
|
-
);
|
|
3744
2421
|
}
|
|
2422
|
+
return results;
|
|
3745
2423
|
}
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
}
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
2424
|
+
async function searchColumns(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2425
|
+
const regex = likePatternToRegex(pattern);
|
|
2426
|
+
const results = [];
|
|
2427
|
+
let schemasToSearch;
|
|
2428
|
+
if (schemaFilter) {
|
|
2429
|
+
schemasToSearch = [schemaFilter];
|
|
2430
|
+
} else {
|
|
2431
|
+
schemasToSearch = await connector.getSchemas();
|
|
2432
|
+
}
|
|
2433
|
+
for (const schemaName of schemasToSearch) {
|
|
2434
|
+
if (results.length >= limit) break;
|
|
2435
|
+
try {
|
|
2436
|
+
const tables = await connector.getTables(schemaName);
|
|
2437
|
+
for (const tableName of tables) {
|
|
2438
|
+
if (results.length >= limit) break;
|
|
2439
|
+
try {
|
|
2440
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
2441
|
+
const matchedColumns = columns.filter((col) => regex.test(col.column_name));
|
|
2442
|
+
for (const column of matchedColumns) {
|
|
2443
|
+
if (results.length >= limit) break;
|
|
2444
|
+
if (detailLevel === "names") {
|
|
2445
|
+
results.push({
|
|
2446
|
+
name: column.column_name,
|
|
2447
|
+
table: tableName,
|
|
2448
|
+
schema: schemaName
|
|
2449
|
+
});
|
|
2450
|
+
} else {
|
|
2451
|
+
results.push({
|
|
2452
|
+
name: column.column_name,
|
|
2453
|
+
table: tableName,
|
|
2454
|
+
schema: schemaName,
|
|
2455
|
+
type: column.data_type,
|
|
2456
|
+
nullable: column.is_nullable === "YES",
|
|
2457
|
+
default: column.column_default
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
} catch (error) {
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
3766
2464
|
}
|
|
2465
|
+
} catch (error) {
|
|
2466
|
+
continue;
|
|
3767
2467
|
}
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
2468
|
+
}
|
|
2469
|
+
return results;
|
|
2470
|
+
}
|
|
2471
|
+
async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2472
|
+
const regex = likePatternToRegex(pattern);
|
|
2473
|
+
const results = [];
|
|
2474
|
+
let schemasToSearch;
|
|
2475
|
+
if (schemaFilter) {
|
|
2476
|
+
schemasToSearch = [schemaFilter];
|
|
2477
|
+
} else {
|
|
2478
|
+
schemasToSearch = await connector.getSchemas();
|
|
2479
|
+
}
|
|
2480
|
+
for (const schemaName of schemasToSearch) {
|
|
2481
|
+
if (results.length >= limit) break;
|
|
2482
|
+
try {
|
|
2483
|
+
const procedures = await connector.getStoredProcedures(schemaName);
|
|
2484
|
+
const matched = procedures.filter((proc) => regex.test(proc));
|
|
2485
|
+
for (const procName of matched) {
|
|
2486
|
+
if (results.length >= limit) break;
|
|
2487
|
+
if (detailLevel === "names") {
|
|
2488
|
+
results.push({
|
|
2489
|
+
name: procName,
|
|
2490
|
+
schema: schemaName
|
|
2491
|
+
});
|
|
2492
|
+
} else {
|
|
2493
|
+
try {
|
|
2494
|
+
const details = await connector.getStoredProcedureDetail(procName, schemaName);
|
|
2495
|
+
results.push({
|
|
2496
|
+
name: procName,
|
|
2497
|
+
schema: schemaName,
|
|
2498
|
+
type: details.procedure_type,
|
|
2499
|
+
language: details.language,
|
|
2500
|
+
parameters: detailLevel === "full" ? details.parameter_list : void 0,
|
|
2501
|
+
return_type: details.return_type,
|
|
2502
|
+
definition: detailLevel === "full" ? details.definition : void 0
|
|
2503
|
+
});
|
|
2504
|
+
} catch (error) {
|
|
2505
|
+
results.push({
|
|
2506
|
+
name: procName,
|
|
2507
|
+
schema: schemaName,
|
|
2508
|
+
error: `Unable to fetch details: ${error.message}`
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
3779
2511
|
}
|
|
3780
|
-
const schemaInfo = schema ? ` in schema '${schema}'` : "";
|
|
3781
|
-
const tableDescription = `Table: ${matchingTable}${schemaInfo}
|
|
3782
|
-
|
|
3783
|
-
Structure:
|
|
3784
|
-
${columns.map((col) => `- ${col.column_name} (${col.data_type})${col.is_nullable === "YES" ? ", nullable" : ""}${col.column_default ? `, default: ${col.column_default}` : ""}`).join("\n")}
|
|
3785
|
-
|
|
3786
|
-
Purpose:
|
|
3787
|
-
This table appears to store ${determineTablePurpose(matchingTable, columns)}
|
|
3788
|
-
|
|
3789
|
-
Relationships:
|
|
3790
|
-
${determineRelationships(matchingTable, columns)}`;
|
|
3791
|
-
return formatPromptSuccessResponse(tableDescription);
|
|
3792
|
-
} catch (error) {
|
|
3793
|
-
return formatPromptErrorResponse(
|
|
3794
|
-
`Error retrieving schema for table '${matchingTable}': ${error.message}`,
|
|
3795
|
-
"TABLE_SCHEMA_ERROR"
|
|
3796
|
-
);
|
|
3797
2512
|
}
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
continue;
|
|
3798
2515
|
}
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
2516
|
+
}
|
|
2517
|
+
return results;
|
|
2518
|
+
}
|
|
2519
|
+
async function searchIndexes(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2520
|
+
const regex = likePatternToRegex(pattern);
|
|
2521
|
+
const results = [];
|
|
2522
|
+
let schemasToSearch;
|
|
2523
|
+
if (schemaFilter) {
|
|
2524
|
+
schemasToSearch = [schemaFilter];
|
|
2525
|
+
} else {
|
|
2526
|
+
schemasToSearch = await connector.getSchemas();
|
|
2527
|
+
}
|
|
2528
|
+
for (const schemaName of schemasToSearch) {
|
|
2529
|
+
if (results.length >= limit) break;
|
|
2530
|
+
try {
|
|
2531
|
+
const tables = await connector.getTables(schemaName);
|
|
2532
|
+
for (const tableName of tables) {
|
|
2533
|
+
if (results.length >= limit) break;
|
|
2534
|
+
try {
|
|
2535
|
+
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
2536
|
+
const matchedIndexes = indexes.filter((idx) => regex.test(idx.index_name));
|
|
2537
|
+
for (const index of matchedIndexes) {
|
|
2538
|
+
if (results.length >= limit) break;
|
|
2539
|
+
if (detailLevel === "names") {
|
|
2540
|
+
results.push({
|
|
2541
|
+
name: index.index_name,
|
|
2542
|
+
table: tableName,
|
|
2543
|
+
schema: schemaName
|
|
2544
|
+
});
|
|
2545
|
+
} else {
|
|
2546
|
+
results.push({
|
|
2547
|
+
name: index.index_name,
|
|
2548
|
+
table: tableName,
|
|
2549
|
+
schema: schemaName,
|
|
2550
|
+
columns: index.column_names,
|
|
2551
|
+
unique: index.is_unique,
|
|
2552
|
+
primary: index.is_primary
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
} catch (error) {
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
3807
2559
|
}
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
return results;
|
|
2565
|
+
}
|
|
2566
|
+
function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
2567
|
+
return async (args, _extra) => {
|
|
2568
|
+
const {
|
|
2569
|
+
object_type,
|
|
2570
|
+
pattern = "%",
|
|
2571
|
+
schema,
|
|
2572
|
+
detail_level = "names",
|
|
2573
|
+
limit = 100
|
|
2574
|
+
} = args;
|
|
2575
|
+
try {
|
|
2576
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
2577
|
+
if (schema) {
|
|
2578
|
+
const schemas = await connector.getSchemas();
|
|
2579
|
+
if (!schemas.includes(schema)) {
|
|
2580
|
+
return createToolErrorResponse(
|
|
2581
|
+
`Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`,
|
|
2582
|
+
"SCHEMA_NOT_FOUND"
|
|
3827
2583
|
);
|
|
3828
2584
|
}
|
|
3829
|
-
} catch (error) {
|
|
3830
|
-
return formatPromptErrorResponse(
|
|
3831
|
-
`Error accessing table schema: ${error.message}`,
|
|
3832
|
-
"SCHEMA_ACCESS_ERROR"
|
|
3833
|
-
);
|
|
3834
2585
|
}
|
|
2586
|
+
let results = [];
|
|
2587
|
+
switch (object_type) {
|
|
2588
|
+
case "schema":
|
|
2589
|
+
results = await searchSchemas(connector, pattern, detail_level, limit);
|
|
2590
|
+
break;
|
|
2591
|
+
case "table":
|
|
2592
|
+
results = await searchTables(connector, pattern, schema, detail_level, limit);
|
|
2593
|
+
break;
|
|
2594
|
+
case "column":
|
|
2595
|
+
results = await searchColumns(connector, pattern, schema, detail_level, limit);
|
|
2596
|
+
break;
|
|
2597
|
+
case "procedure":
|
|
2598
|
+
results = await searchProcedures(connector, pattern, schema, detail_level, limit);
|
|
2599
|
+
break;
|
|
2600
|
+
case "index":
|
|
2601
|
+
results = await searchIndexes(connector, pattern, schema, detail_level, limit);
|
|
2602
|
+
break;
|
|
2603
|
+
default:
|
|
2604
|
+
return createToolErrorResponse(
|
|
2605
|
+
`Unsupported object_type: ${object_type}`,
|
|
2606
|
+
"INVALID_OBJECT_TYPE"
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
return createToolSuccessResponse({
|
|
2610
|
+
object_type,
|
|
2611
|
+
pattern,
|
|
2612
|
+
schema,
|
|
2613
|
+
detail_level,
|
|
2614
|
+
count: results.length,
|
|
2615
|
+
results,
|
|
2616
|
+
truncated: results.length === limit
|
|
2617
|
+
});
|
|
2618
|
+
} catch (error) {
|
|
2619
|
+
return createToolErrorResponse(
|
|
2620
|
+
`Error searching database objects: ${error.message}`,
|
|
2621
|
+
"SEARCH_ERROR"
|
|
2622
|
+
);
|
|
3835
2623
|
}
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
let dbOverview = `Database Overview ${schemaInfo}
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
3839
2626
|
|
|
3840
|
-
|
|
3841
|
-
|
|
2627
|
+
// src/utils/tool-metadata.ts
|
|
2628
|
+
import { z as z3 } from "zod";
|
|
2629
|
+
|
|
2630
|
+
// src/utils/normalize-id.ts
|
|
2631
|
+
function normalizeSourceId(id) {
|
|
2632
|
+
return id.replace(/[^a-zA-Z0-9]/g, "_");
|
|
2633
|
+
}
|
|
3842
2634
|
|
|
3843
|
-
|
|
3844
|
-
|
|
2635
|
+
// src/utils/tool-metadata.ts
|
|
2636
|
+
function zodToParameters(schema) {
|
|
2637
|
+
const parameters = [];
|
|
2638
|
+
for (const [key, zodType] of Object.entries(schema)) {
|
|
2639
|
+
const description = zodType.description || "";
|
|
2640
|
+
const required = !(zodType instanceof z3.ZodOptional);
|
|
2641
|
+
let type = "string";
|
|
2642
|
+
if (zodType instanceof z3.ZodString) {
|
|
2643
|
+
type = "string";
|
|
2644
|
+
} else if (zodType instanceof z3.ZodNumber) {
|
|
2645
|
+
type = "number";
|
|
2646
|
+
} else if (zodType instanceof z3.ZodBoolean) {
|
|
2647
|
+
type = "boolean";
|
|
2648
|
+
} else if (zodType instanceof z3.ZodArray) {
|
|
2649
|
+
type = "array";
|
|
2650
|
+
} else if (zodType instanceof z3.ZodObject) {
|
|
2651
|
+
type = "object";
|
|
2652
|
+
}
|
|
2653
|
+
parameters.push({
|
|
2654
|
+
name: key,
|
|
2655
|
+
type,
|
|
2656
|
+
required,
|
|
2657
|
+
description
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
return parameters;
|
|
2661
|
+
}
|
|
2662
|
+
function getExecuteSqlMetadata(sourceId) {
|
|
2663
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2664
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2665
|
+
const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
|
|
2666
|
+
const dbType = sourceConfig.type;
|
|
2667
|
+
const toolName = sourceId === "default" ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
2668
|
+
const isDefault = sourceIds[0] === sourceId;
|
|
2669
|
+
const title = isDefault ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
2670
|
+
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
2671
|
+
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
2672
|
+
const description = `Execute SQL queries on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}${readonlyNote}${maxRowsNote}`;
|
|
2673
|
+
const isReadonly = executeOptions.readonly === true;
|
|
2674
|
+
const annotations = {
|
|
2675
|
+
title,
|
|
2676
|
+
readOnlyHint: isReadonly,
|
|
2677
|
+
destructiveHint: !isReadonly,
|
|
2678
|
+
// Can be destructive if not readonly
|
|
2679
|
+
// In readonly mode, queries are more predictable (though still not strictly idempotent due to data changes)
|
|
2680
|
+
// In write mode, queries are definitely not idempotent
|
|
2681
|
+
idempotentHint: false,
|
|
2682
|
+
// In readonly mode, it's safer to operate on arbitrary tables (just reading)
|
|
2683
|
+
// In write mode, operating on arbitrary tables is more dangerous
|
|
2684
|
+
openWorldHint: isReadonly
|
|
2685
|
+
};
|
|
2686
|
+
return {
|
|
2687
|
+
name: toolName,
|
|
2688
|
+
description,
|
|
2689
|
+
schema: executeSqlSchema,
|
|
2690
|
+
annotations
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
function getSearchObjectsMetadata(sourceId, dbType, isDefault) {
|
|
2694
|
+
const toolName = sourceId === "default" ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
|
|
2695
|
+
const title = isDefault ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
2696
|
+
const description = `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}. Supports SQL LIKE patterns (default: '%' for all), filtering, and token-efficient progressive disclosure.`;
|
|
2697
|
+
return {
|
|
2698
|
+
name: toolName,
|
|
2699
|
+
description,
|
|
2700
|
+
title
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
function customParamsToToolParams(params) {
|
|
2704
|
+
if (!params || params.length === 0) {
|
|
2705
|
+
return [];
|
|
2706
|
+
}
|
|
2707
|
+
return params.map((param) => ({
|
|
2708
|
+
name: param.name,
|
|
2709
|
+
type: param.type,
|
|
2710
|
+
required: param.required !== false && param.default === void 0,
|
|
2711
|
+
description: param.description
|
|
2712
|
+
}));
|
|
2713
|
+
}
|
|
2714
|
+
function getToolsForSource(sourceId) {
|
|
2715
|
+
const tools = [];
|
|
2716
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2717
|
+
const dbType = sourceConfig.type;
|
|
2718
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2719
|
+
const isDefault = sourceIds[0] === sourceId;
|
|
2720
|
+
const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
|
|
2721
|
+
const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
|
|
2722
|
+
tools.push({
|
|
2723
|
+
name: executeSqlMetadata.name,
|
|
2724
|
+
description: executeSqlMetadata.description,
|
|
2725
|
+
parameters: executeSqlParameters
|
|
2726
|
+
});
|
|
2727
|
+
const searchMetadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
|
|
2728
|
+
tools.push({
|
|
2729
|
+
name: searchMetadata.name,
|
|
2730
|
+
description: searchMetadata.description,
|
|
2731
|
+
parameters: [
|
|
2732
|
+
{
|
|
2733
|
+
name: "object_type",
|
|
2734
|
+
type: "string",
|
|
2735
|
+
required: true,
|
|
2736
|
+
description: "Type of database object to search for (schema, table, column, procedure, index)"
|
|
2737
|
+
},
|
|
2738
|
+
{
|
|
2739
|
+
name: "pattern",
|
|
2740
|
+
type: "string",
|
|
2741
|
+
required: false,
|
|
2742
|
+
description: "Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all)."
|
|
2743
|
+
},
|
|
2744
|
+
{
|
|
2745
|
+
name: "schema",
|
|
2746
|
+
type: "string",
|
|
2747
|
+
required: false,
|
|
2748
|
+
description: "Filter results to a specific schema/database"
|
|
2749
|
+
},
|
|
2750
|
+
{
|
|
2751
|
+
name: "detail_level",
|
|
2752
|
+
type: "string",
|
|
2753
|
+
required: false,
|
|
2754
|
+
description: "Level of detail to return: names (minimal), summary (with metadata), full (complete structure). Defaults to 'names'."
|
|
2755
|
+
},
|
|
2756
|
+
{
|
|
2757
|
+
name: "limit",
|
|
2758
|
+
type: "integer",
|
|
2759
|
+
required: false,
|
|
2760
|
+
description: "Maximum number of results to return (default: 100, max: 1000)"
|
|
2761
|
+
}
|
|
2762
|
+
]
|
|
2763
|
+
});
|
|
2764
|
+
if (customToolRegistry.isInitialized()) {
|
|
2765
|
+
const customTools = customToolRegistry.getTools();
|
|
2766
|
+
const sourceCustomTools = customTools.filter((tool) => tool.source === sourceId);
|
|
2767
|
+
for (const customTool of sourceCustomTools) {
|
|
2768
|
+
tools.push({
|
|
2769
|
+
name: customTool.name,
|
|
2770
|
+
description: customTool.description,
|
|
2771
|
+
parameters: customParamsToToolParams(customTool.parameters)
|
|
2772
|
+
});
|
|
3845
2773
|
}
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
);
|
|
3850
|
-
if (possibleTableMatches.length > 0) {
|
|
3851
|
-
return formatPromptSuccessResponse(
|
|
3852
|
-
`Table "${table}" not found. Did you mean one of these tables?
|
|
2774
|
+
}
|
|
2775
|
+
return tools;
|
|
2776
|
+
}
|
|
3853
2777
|
|
|
3854
|
-
|
|
3855
|
-
|
|
2778
|
+
// src/tools/custom-tool-handler.ts
|
|
2779
|
+
import { z as z4 } from "zod";
|
|
2780
|
+
function buildZodSchemaFromParameters(parameters) {
|
|
2781
|
+
if (!parameters || parameters.length === 0) {
|
|
2782
|
+
return {};
|
|
2783
|
+
}
|
|
2784
|
+
const schemaShape = {};
|
|
2785
|
+
for (const param of parameters) {
|
|
2786
|
+
let fieldSchema;
|
|
2787
|
+
switch (param.type) {
|
|
2788
|
+
case "string":
|
|
2789
|
+
fieldSchema = z4.string().describe(param.description);
|
|
2790
|
+
break;
|
|
2791
|
+
case "integer":
|
|
2792
|
+
fieldSchema = z4.number().int().describe(param.description);
|
|
2793
|
+
break;
|
|
2794
|
+
case "float":
|
|
2795
|
+
fieldSchema = z4.number().describe(param.description);
|
|
2796
|
+
break;
|
|
2797
|
+
case "boolean":
|
|
2798
|
+
fieldSchema = z4.boolean().describe(param.description);
|
|
2799
|
+
break;
|
|
2800
|
+
case "array":
|
|
2801
|
+
fieldSchema = z4.array(z4.unknown()).describe(param.description);
|
|
2802
|
+
break;
|
|
2803
|
+
default:
|
|
2804
|
+
throw new Error(`Unsupported parameter type: ${param.type}`);
|
|
2805
|
+
}
|
|
2806
|
+
if (param.allowed_values && param.allowed_values.length > 0) {
|
|
2807
|
+
if (param.type === "string") {
|
|
2808
|
+
fieldSchema = z4.enum(param.allowed_values).describe(param.description);
|
|
3856
2809
|
} else {
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
2810
|
+
fieldSchema = fieldSchema.refine(
|
|
2811
|
+
(val) => param.allowed_values.includes(val),
|
|
2812
|
+
{
|
|
2813
|
+
message: `Value must be one of: ${param.allowed_values.join(", ")}`
|
|
2814
|
+
}
|
|
3861
2815
|
);
|
|
3862
2816
|
}
|
|
3863
2817
|
}
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
);
|
|
3869
|
-
}
|
|
3870
|
-
return formatPromptErrorResponse(
|
|
3871
|
-
`Unable to process request for schema: ${schema}, table: ${table}`,
|
|
3872
|
-
"UNHANDLED_REQUEST"
|
|
3873
|
-
);
|
|
3874
|
-
}
|
|
3875
|
-
function determineTablePurpose(tableName, columns) {
|
|
3876
|
-
const lowerTableName = tableName.toLowerCase();
|
|
3877
|
-
const columnNames = columns.map((c) => c.column_name.toLowerCase());
|
|
3878
|
-
if (lowerTableName.includes("user") || columnNames.includes("username") || columnNames.includes("email")) {
|
|
3879
|
-
return "user information and profiles";
|
|
3880
|
-
}
|
|
3881
|
-
if (lowerTableName.includes("order") || lowerTableName.includes("purchase")) {
|
|
3882
|
-
return "order or purchase transactions";
|
|
3883
|
-
}
|
|
3884
|
-
if (lowerTableName.includes("product") || lowerTableName.includes("item")) {
|
|
3885
|
-
return "product or item information";
|
|
3886
|
-
}
|
|
3887
|
-
if (lowerTableName.includes("log") || columnNames.includes("timestamp")) {
|
|
3888
|
-
return "event or activity logs";
|
|
3889
|
-
}
|
|
3890
|
-
if (columnNames.includes("created_at") && columnNames.includes("updated_at")) {
|
|
3891
|
-
return "tracking timestamped data records";
|
|
2818
|
+
if (param.default !== void 0 || param.required === false) {
|
|
2819
|
+
fieldSchema = fieldSchema.optional();
|
|
2820
|
+
}
|
|
2821
|
+
schemaShape[param.name] = fieldSchema;
|
|
3892
2822
|
}
|
|
3893
|
-
return
|
|
2823
|
+
return schemaShape;
|
|
3894
2824
|
}
|
|
3895
|
-
function
|
|
3896
|
-
const
|
|
3897
|
-
const
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
2825
|
+
function createCustomToolHandler(toolConfig) {
|
|
2826
|
+
const zodSchemaShape = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
2827
|
+
const zodSchema = z4.object(zodSchemaShape);
|
|
2828
|
+
return async (args, extra) => {
|
|
2829
|
+
const startTime = Date.now();
|
|
2830
|
+
let success = true;
|
|
2831
|
+
let errorMessage;
|
|
2832
|
+
let paramValues = [];
|
|
2833
|
+
try {
|
|
2834
|
+
const validatedArgs = zodSchema.parse(args);
|
|
2835
|
+
const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
|
|
2836
|
+
const executeOptions = ConnectorManager.getCurrentExecuteOptions(
|
|
2837
|
+
toolConfig.source
|
|
3905
2838
|
);
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
2839
|
+
const isReadonly = executeOptions.readonly === true;
|
|
2840
|
+
if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
|
|
2841
|
+
errorMessage = `Tool '${toolConfig.name}' cannot execute in readonly mode for source '${toolConfig.source}'. Only read-only SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
2842
|
+
success = false;
|
|
2843
|
+
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
2844
|
+
}
|
|
2845
|
+
paramValues = mapArgumentsToArray(
|
|
2846
|
+
toolConfig.parameters,
|
|
2847
|
+
validatedArgs
|
|
2848
|
+
);
|
|
2849
|
+
const result = await connector.executeSQL(
|
|
2850
|
+
toolConfig.statement,
|
|
2851
|
+
executeOptions,
|
|
2852
|
+
paramValues
|
|
2853
|
+
);
|
|
2854
|
+
const responseData = {
|
|
2855
|
+
rows: result.rows,
|
|
2856
|
+
count: result.rows.length,
|
|
2857
|
+
source_id: toolConfig.source
|
|
2858
|
+
};
|
|
2859
|
+
return createToolSuccessResponse(responseData);
|
|
2860
|
+
} catch (error) {
|
|
2861
|
+
success = false;
|
|
2862
|
+
errorMessage = error.message;
|
|
2863
|
+
if (error instanceof z4.ZodError) {
|
|
2864
|
+
const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
2865
|
+
errorMessage = `Parameter validation failed: ${issues}`;
|
|
2866
|
+
} else {
|
|
2867
|
+
errorMessage = `${errorMessage}
|
|
2868
|
+
|
|
2869
|
+
SQL: ${toolConfig.statement}
|
|
2870
|
+
Parameters: ${JSON.stringify(paramValues)}`;
|
|
2871
|
+
}
|
|
2872
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
2873
|
+
} finally {
|
|
2874
|
+
requestStore.add({
|
|
2875
|
+
id: crypto.randomUUID(),
|
|
2876
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2877
|
+
sourceId: toolConfig.source,
|
|
2878
|
+
toolName: toolConfig.name,
|
|
2879
|
+
sql: toolConfig.statement,
|
|
2880
|
+
durationMs: Date.now() - startTime,
|
|
2881
|
+
client: getClientIdentifier(extra),
|
|
2882
|
+
success,
|
|
2883
|
+
error: errorMessage
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
3914
2887
|
}
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
return "Timestamp for when the record was last updated";
|
|
3938
|
-
}
|
|
3939
|
-
if (lowerColumnName.includes("date") || lowerColumnName.includes("time")) {
|
|
3940
|
-
return "Stores date or time information";
|
|
3941
|
-
}
|
|
3942
|
-
if (lowerColumnName.includes("price") || lowerColumnName.includes("cost") || lowerColumnName.includes("amount")) {
|
|
3943
|
-
return "Stores monetary value information";
|
|
3944
|
-
}
|
|
3945
|
-
if (dataType.includes("boolean")) {
|
|
3946
|
-
return "Stores a true/false flag";
|
|
3947
|
-
}
|
|
3948
|
-
if (dataType.includes("json")) {
|
|
3949
|
-
return "Stores structured JSON data";
|
|
3950
|
-
}
|
|
3951
|
-
if (dataType.includes("text") || dataType.includes("varchar") || dataType.includes("char")) {
|
|
3952
|
-
return "Stores text information";
|
|
2888
|
+
|
|
2889
|
+
// src/tools/index.ts
|
|
2890
|
+
function registerTools(server) {
|
|
2891
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2892
|
+
if (sourceIds.length === 0) {
|
|
2893
|
+
throw new Error("No database sources configured");
|
|
2894
|
+
}
|
|
2895
|
+
const registry = getToolRegistry();
|
|
2896
|
+
for (const sourceId of sourceIds) {
|
|
2897
|
+
const enabledTools = registry.getToolsForSource(sourceId);
|
|
2898
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2899
|
+
const dbType = sourceConfig.type;
|
|
2900
|
+
const isDefault = sourceIds[0] === sourceId;
|
|
2901
|
+
for (const toolConfig of enabledTools) {
|
|
2902
|
+
if (toolConfig.name === BUILTIN_TOOL_EXECUTE_SQL) {
|
|
2903
|
+
registerExecuteSqlTool(server, sourceId, dbType);
|
|
2904
|
+
} else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
|
|
2905
|
+
registerSearchObjectsTool(server, sourceId, dbType, isDefault);
|
|
2906
|
+
} else {
|
|
2907
|
+
registerCustomTool(server, toolConfig, dbType);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
3953
2910
|
}
|
|
3954
|
-
return `Stores ${dataType} data`;
|
|
3955
2911
|
}
|
|
3956
|
-
function
|
|
3957
|
-
const
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
if (tableNames.some((t) => t.includes("employee")) || tableNames.some((t) => t.includes("payroll"))) {
|
|
3968
|
-
return "appears to be related to HR or employee management";
|
|
3969
|
-
}
|
|
3970
|
-
if (tableNames.some((t) => t.includes("inventory")) || tableNames.some((t) => t.includes("stock"))) {
|
|
3971
|
-
return "appears to be related to inventory or stock management";
|
|
3972
|
-
}
|
|
3973
|
-
return "contains multiple tables that store related information";
|
|
2912
|
+
function registerExecuteSqlTool(server, sourceId, dbType) {
|
|
2913
|
+
const metadata = getExecuteSqlMetadata(sourceId);
|
|
2914
|
+
server.registerTool(
|
|
2915
|
+
metadata.name,
|
|
2916
|
+
{
|
|
2917
|
+
description: metadata.description,
|
|
2918
|
+
inputSchema: metadata.schema,
|
|
2919
|
+
annotations: metadata.annotations
|
|
2920
|
+
},
|
|
2921
|
+
createExecuteSqlToolHandler(sourceId)
|
|
2922
|
+
);
|
|
3974
2923
|
}
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
2924
|
+
function registerSearchObjectsTool(server, sourceId, dbType, isDefault) {
|
|
2925
|
+
const metadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
|
|
2926
|
+
server.registerTool(
|
|
2927
|
+
metadata.name,
|
|
2928
|
+
{
|
|
2929
|
+
description: metadata.description,
|
|
2930
|
+
inputSchema: searchDatabaseObjectsSchema,
|
|
2931
|
+
annotations: {
|
|
2932
|
+
title: metadata.title,
|
|
2933
|
+
readOnlyHint: true,
|
|
2934
|
+
destructiveHint: false,
|
|
2935
|
+
idempotentHint: true,
|
|
2936
|
+
openWorldHint: true
|
|
2937
|
+
}
|
|
2938
|
+
},
|
|
2939
|
+
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
3983
2940
|
);
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
2941
|
+
}
|
|
2942
|
+
function registerCustomTool(server, toolConfig, dbType) {
|
|
2943
|
+
const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
|
|
2944
|
+
const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
2945
|
+
server.registerTool(
|
|
2946
|
+
toolConfig.name,
|
|
2947
|
+
{
|
|
2948
|
+
description: toolConfig.description,
|
|
2949
|
+
inputSchema: zodSchema,
|
|
2950
|
+
annotations: {
|
|
2951
|
+
title: `${toolConfig.name} (${dbType})`,
|
|
2952
|
+
readOnlyHint: isReadOnly,
|
|
2953
|
+
destructiveHint: !isReadOnly,
|
|
2954
|
+
idempotentHint: isReadOnly,
|
|
2955
|
+
openWorldHint: false
|
|
2956
|
+
}
|
|
2957
|
+
},
|
|
2958
|
+
createCustomToolHandler(toolConfig)
|
|
3989
2959
|
);
|
|
2960
|
+
console.error(` - ${toolConfig.name} \u2192 ${toolConfig.source} (${dbType})`);
|
|
3990
2961
|
}
|
|
3991
2962
|
|
|
3992
2963
|
// src/api/sources.ts
|
|
@@ -4036,6 +3007,7 @@ function transformSourceConfig(source, isDefault) {
|
|
|
4036
3007
|
}
|
|
4037
3008
|
dataSource.ssh_tunnel = sshTunnel;
|
|
4038
3009
|
}
|
|
3010
|
+
dataSource.tools = getToolsForSource(source.id);
|
|
4039
3011
|
return dataSource;
|
|
4040
3012
|
}
|
|
4041
3013
|
function listSources(req, res) {
|
|
@@ -4079,11 +3051,144 @@ function getSource(req, res) {
|
|
|
4079
3051
|
}
|
|
4080
3052
|
}
|
|
4081
3053
|
|
|
3054
|
+
// src/api/requests.ts
|
|
3055
|
+
function listRequests(req, res) {
|
|
3056
|
+
try {
|
|
3057
|
+
const sourceId = req.query.source_id;
|
|
3058
|
+
const requests = requestStore.getAll(sourceId);
|
|
3059
|
+
res.json({
|
|
3060
|
+
requests,
|
|
3061
|
+
total: requests.length
|
|
3062
|
+
});
|
|
3063
|
+
} catch (error) {
|
|
3064
|
+
console.error("Error listing requests:", error);
|
|
3065
|
+
res.status(500).json({
|
|
3066
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// src/utils/startup-table.ts
|
|
3072
|
+
var BOX = {
|
|
3073
|
+
topLeft: "\u250C",
|
|
3074
|
+
topRight: "\u2510",
|
|
3075
|
+
bottomLeft: "\u2514",
|
|
3076
|
+
bottomRight: "\u2518",
|
|
3077
|
+
horizontal: "\u2500",
|
|
3078
|
+
vertical: "\u2502",
|
|
3079
|
+
leftT: "\u251C",
|
|
3080
|
+
rightT: "\u2524",
|
|
3081
|
+
bullet: "\u2022"
|
|
3082
|
+
};
|
|
3083
|
+
function parseHostAndDatabase(source) {
|
|
3084
|
+
if (source.dsn) {
|
|
3085
|
+
const parsed = parseConnectionInfoFromDSN(source.dsn);
|
|
3086
|
+
if (parsed) {
|
|
3087
|
+
if (parsed.type === "sqlite") {
|
|
3088
|
+
return { host: "", database: parsed.database || ":memory:" };
|
|
3089
|
+
}
|
|
3090
|
+
if (!parsed.host) {
|
|
3091
|
+
return { host: "", database: parsed.database || "" };
|
|
3092
|
+
}
|
|
3093
|
+
const port = parsed.port ?? getDefaultPortForType(parsed.type);
|
|
3094
|
+
const host2 = port ? `${parsed.host}:${port}` : parsed.host;
|
|
3095
|
+
return { host: host2, database: parsed.database || "" };
|
|
3096
|
+
}
|
|
3097
|
+
return { host: "unknown", database: "" };
|
|
3098
|
+
}
|
|
3099
|
+
const host = source.host ? source.port ? `${source.host}:${source.port}` : source.host : "";
|
|
3100
|
+
const database = source.database || "";
|
|
3101
|
+
return { host, database };
|
|
3102
|
+
}
|
|
3103
|
+
function horizontalLine(width, left, right) {
|
|
3104
|
+
return left + BOX.horizontal.repeat(width - 2) + right;
|
|
3105
|
+
}
|
|
3106
|
+
function fitString(str, width) {
|
|
3107
|
+
if (str.length > width) {
|
|
3108
|
+
return str.slice(0, width - 1) + "\u2026";
|
|
3109
|
+
}
|
|
3110
|
+
return str.padEnd(width);
|
|
3111
|
+
}
|
|
3112
|
+
function formatHostDatabase(host, database) {
|
|
3113
|
+
return host ? database ? `${host}/${database}` : host : database || "";
|
|
3114
|
+
}
|
|
3115
|
+
function generateStartupTable(sources) {
|
|
3116
|
+
if (sources.length === 0) {
|
|
3117
|
+
return "";
|
|
3118
|
+
}
|
|
3119
|
+
const idTypeWidth = Math.max(
|
|
3120
|
+
20,
|
|
3121
|
+
...sources.map((s) => `${s.id} (${s.type})`.length)
|
|
3122
|
+
);
|
|
3123
|
+
const hostDbWidth = Math.max(
|
|
3124
|
+
24,
|
|
3125
|
+
...sources.map((s) => formatHostDatabase(s.host, s.database).length)
|
|
3126
|
+
);
|
|
3127
|
+
const modeWidth = Math.max(
|
|
3128
|
+
10,
|
|
3129
|
+
...sources.map((s) => {
|
|
3130
|
+
const modes = [];
|
|
3131
|
+
if (s.isDemo) modes.push("DEMO");
|
|
3132
|
+
if (s.readonly) modes.push("READ-ONLY");
|
|
3133
|
+
return modes.join(" ").length;
|
|
3134
|
+
})
|
|
3135
|
+
);
|
|
3136
|
+
const totalWidth = 2 + idTypeWidth + 3 + hostDbWidth + 3 + modeWidth + 2;
|
|
3137
|
+
const lines = [];
|
|
3138
|
+
for (let i = 0; i < sources.length; i++) {
|
|
3139
|
+
const source = sources[i];
|
|
3140
|
+
const isFirst = i === 0;
|
|
3141
|
+
const isLast = i === sources.length - 1;
|
|
3142
|
+
if (isFirst) {
|
|
3143
|
+
lines.push(horizontalLine(totalWidth, BOX.topLeft, BOX.topRight));
|
|
3144
|
+
}
|
|
3145
|
+
const idType = fitString(`${source.id} (${source.type})`, idTypeWidth);
|
|
3146
|
+
const hostDb = fitString(
|
|
3147
|
+
formatHostDatabase(source.host, source.database),
|
|
3148
|
+
hostDbWidth
|
|
3149
|
+
);
|
|
3150
|
+
const modes = [];
|
|
3151
|
+
if (source.isDemo) modes.push("DEMO");
|
|
3152
|
+
if (source.readonly) modes.push("READ-ONLY");
|
|
3153
|
+
const modeStr = fitString(modes.join(" "), modeWidth);
|
|
3154
|
+
lines.push(
|
|
3155
|
+
`${BOX.vertical} ${idType} ${BOX.vertical} ${hostDb} ${BOX.vertical} ${modeStr} ${BOX.vertical}`
|
|
3156
|
+
);
|
|
3157
|
+
lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
|
|
3158
|
+
for (const tool of source.tools) {
|
|
3159
|
+
const toolLine = ` ${BOX.bullet} ${tool}`;
|
|
3160
|
+
lines.push(
|
|
3161
|
+
`${BOX.vertical} ${fitString(toolLine, totalWidth - 4)} ${BOX.vertical}`
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
if (isLast) {
|
|
3165
|
+
lines.push(horizontalLine(totalWidth, BOX.bottomLeft, BOX.bottomRight));
|
|
3166
|
+
} else {
|
|
3167
|
+
lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
return lines.join("\n");
|
|
3171
|
+
}
|
|
3172
|
+
function buildSourceDisplayInfo(sourceConfigs, getToolsForSource2, isDemo) {
|
|
3173
|
+
return sourceConfigs.map((source) => {
|
|
3174
|
+
const { host, database } = parseHostAndDatabase(source);
|
|
3175
|
+
return {
|
|
3176
|
+
id: source.id,
|
|
3177
|
+
type: source.type || "sqlite",
|
|
3178
|
+
host,
|
|
3179
|
+
database,
|
|
3180
|
+
readonly: source.readonly || false,
|
|
3181
|
+
isDemo,
|
|
3182
|
+
tools: getToolsForSource2(source.id)
|
|
3183
|
+
};
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
|
|
4082
3187
|
// src/server.ts
|
|
4083
|
-
var
|
|
4084
|
-
var
|
|
4085
|
-
var packageJsonPath =
|
|
4086
|
-
var packageJson = JSON.parse(
|
|
3188
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
3189
|
+
var __dirname = path.dirname(__filename);
|
|
3190
|
+
var packageJsonPath = path.join(__dirname, "..", "package.json");
|
|
3191
|
+
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
4087
3192
|
var SERVER_NAME = "DBHub MCP Server";
|
|
4088
3193
|
var SERVER_VERSION = packageJson.version;
|
|
4089
3194
|
function generateBanner(version, modes = []) {
|
|
@@ -4101,12 +3206,10 @@ v${version}${modeText} - Universal Database MCP Server
|
|
|
4101
3206
|
}
|
|
4102
3207
|
async function main() {
|
|
4103
3208
|
try {
|
|
4104
|
-
const idData = resolveId();
|
|
4105
|
-
const id = idData?.id;
|
|
4106
3209
|
const sourceConfigsData = await resolveSourceConfigs();
|
|
4107
3210
|
if (!sourceConfigsData) {
|
|
4108
3211
|
const samples = ConnectorRegistry.getAllSampleDSNs();
|
|
4109
|
-
const sampleFormats = Object.entries(samples).map(([
|
|
3212
|
+
const sampleFormats = Object.entries(samples).map(([id, dsn]) => ` - ${id}: ${dsn}`).join("\n");
|
|
4110
3213
|
console.error(`
|
|
4111
3214
|
ERROR: Database connection configuration is required.
|
|
4112
3215
|
Please provide configuration in one of these ways (in order of priority):
|
|
@@ -4129,35 +3232,44 @@ See documentation for more details on configuring database connections.
|
|
|
4129
3232
|
`);
|
|
4130
3233
|
process.exit(1);
|
|
4131
3234
|
}
|
|
4132
|
-
const createServer2 = () => {
|
|
4133
|
-
const server = new McpServer2({
|
|
4134
|
-
name: SERVER_NAME,
|
|
4135
|
-
version: SERVER_VERSION
|
|
4136
|
-
});
|
|
4137
|
-
registerResources(server);
|
|
4138
|
-
registerTools(server, id);
|
|
4139
|
-
registerPrompts(server);
|
|
4140
|
-
return server;
|
|
4141
|
-
};
|
|
4142
3235
|
const connectorManager = new ConnectorManager();
|
|
4143
3236
|
const sources = sourceConfigsData.sources;
|
|
4144
3237
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
4145
|
-
if (idData) {
|
|
4146
|
-
console.error(`ID: ${idData.id} (from ${idData.source})`);
|
|
4147
|
-
}
|
|
4148
3238
|
console.error(`Connecting to ${sources.length} database source(s)...`);
|
|
4149
3239
|
for (const source of sources) {
|
|
4150
3240
|
const dsn = source.dsn || buildDSNFromSource(source);
|
|
4151
3241
|
console.error(` - ${source.id}: ${redactDSN(dsn)}`);
|
|
4152
3242
|
}
|
|
4153
3243
|
await connectorManager.connectWithSources(sources);
|
|
4154
|
-
const
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
3244
|
+
const { initializeToolRegistry } = await import("./registry-GJGPWR3I.js");
|
|
3245
|
+
initializeToolRegistry({
|
|
3246
|
+
sources: sourceConfigsData.sources,
|
|
3247
|
+
tools: sourceConfigsData.tools
|
|
3248
|
+
});
|
|
3249
|
+
console.error("Tool registry initialized");
|
|
3250
|
+
if (sourceConfigsData.tools && sourceConfigsData.tools.length > 0) {
|
|
3251
|
+
const { customToolRegistry: customToolRegistry2 } = await import("./custom-tool-registry-EW3KOBGC.js");
|
|
3252
|
+
const { BUILTIN_TOOLS } = await import("./builtin-tools-SVENYBIA.js");
|
|
3253
|
+
if (!customToolRegistry2.isInitialized()) {
|
|
3254
|
+
const customTools = sourceConfigsData.tools.filter(
|
|
3255
|
+
(tool) => !BUILTIN_TOOLS.includes(tool.name)
|
|
3256
|
+
);
|
|
3257
|
+
if (customTools.length > 0) {
|
|
3258
|
+
customToolRegistry2.initialize(customTools);
|
|
3259
|
+
console.error(`Custom tool registry initialized with ${customTools.length} tool(s)`);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
4159
3262
|
}
|
|
4160
|
-
const
|
|
3263
|
+
const createServer = () => {
|
|
3264
|
+
const server = new McpServer({
|
|
3265
|
+
name: SERVER_NAME,
|
|
3266
|
+
version: SERVER_VERSION
|
|
3267
|
+
});
|
|
3268
|
+
registerTools(server);
|
|
3269
|
+
return server;
|
|
3270
|
+
};
|
|
3271
|
+
const transportData = resolveTransport();
|
|
3272
|
+
const port = transportData.type === "http" ? resolvePort().port : null;
|
|
4161
3273
|
const activeModes = [];
|
|
4162
3274
|
const modeDescriptions = [];
|
|
4163
3275
|
const isDemo = isDemoMode();
|
|
@@ -4165,19 +3277,17 @@ See documentation for more details on configuring database connections.
|
|
|
4165
3277
|
activeModes.push("DEMO");
|
|
4166
3278
|
modeDescriptions.push("using sample employee database");
|
|
4167
3279
|
}
|
|
4168
|
-
if (readonly) {
|
|
4169
|
-
activeModes.push("READ-ONLY");
|
|
4170
|
-
modeDescriptions.push("only read only queries allowed");
|
|
4171
|
-
}
|
|
4172
|
-
if (sources.length > 1) {
|
|
4173
|
-
console.error(`Multi-source mode: ${sources.length} databases configured`);
|
|
4174
|
-
}
|
|
4175
3280
|
if (activeModes.length > 0) {
|
|
4176
3281
|
console.error(`Running in ${activeModes.join(" and ")} mode - ${modeDescriptions.join(", ")}`);
|
|
4177
3282
|
}
|
|
4178
3283
|
console.error(generateBanner(SERVER_VERSION, activeModes));
|
|
3284
|
+
const sourceDisplayInfos = buildSourceDisplayInfo(
|
|
3285
|
+
sources,
|
|
3286
|
+
(sourceId) => getToolsForSource(sourceId).map((t) => t.name),
|
|
3287
|
+
isDemo
|
|
3288
|
+
);
|
|
3289
|
+
console.error(generateStartupTable(sourceDisplayInfos));
|
|
4179
3290
|
if (transportData.type === "http") {
|
|
4180
|
-
const port = portData.port;
|
|
4181
3291
|
const app = express();
|
|
4182
3292
|
app.use(express.json());
|
|
4183
3293
|
app.use((req, res, next) => {
|
|
@@ -4194,13 +3304,14 @@ See documentation for more details on configuring database connections.
|
|
|
4194
3304
|
}
|
|
4195
3305
|
next();
|
|
4196
3306
|
});
|
|
4197
|
-
const frontendPath =
|
|
3307
|
+
const frontendPath = path.join(__dirname, "public");
|
|
4198
3308
|
app.use(express.static(frontendPath));
|
|
4199
3309
|
app.get("/healthz", (req, res) => {
|
|
4200
3310
|
res.status(200).send("OK");
|
|
4201
3311
|
});
|
|
4202
3312
|
app.get("/api/sources", listSources);
|
|
4203
3313
|
app.get("/api/sources/:sourceId", getSource);
|
|
3314
|
+
app.get("/api/requests", listRequests);
|
|
4204
3315
|
app.get("/mcp", (req, res) => {
|
|
4205
3316
|
res.status(405).json({
|
|
4206
3317
|
error: "Method Not Allowed",
|
|
@@ -4215,7 +3326,7 @@ See documentation for more details on configuring database connections.
|
|
|
4215
3326
|
enableJsonResponse: true
|
|
4216
3327
|
// Use JSON responses (SSE not supported in stateless mode)
|
|
4217
3328
|
});
|
|
4218
|
-
const server =
|
|
3329
|
+
const server = createServer();
|
|
4219
3330
|
await server.connect(transport);
|
|
4220
3331
|
await transport.handleRequest(req, res, req.body);
|
|
4221
3332
|
} catch (error) {
|
|
@@ -4227,7 +3338,7 @@ See documentation for more details on configuring database connections.
|
|
|
4227
3338
|
});
|
|
4228
3339
|
if (process.env.NODE_ENV !== "development") {
|
|
4229
3340
|
app.get("*", (req, res) => {
|
|
4230
|
-
res.sendFile(
|
|
3341
|
+
res.sendFile(path.join(frontendPath, "index.html"));
|
|
4231
3342
|
});
|
|
4232
3343
|
}
|
|
4233
3344
|
app.listen(port, "0.0.0.0", () => {
|
|
@@ -4242,7 +3353,7 @@ See documentation for more details on configuring database connections.
|
|
|
4242
3353
|
console.error(`MCP server endpoint at http://0.0.0.0:${port}/mcp`);
|
|
4243
3354
|
});
|
|
4244
3355
|
} else {
|
|
4245
|
-
const server =
|
|
3356
|
+
const server = createServer();
|
|
4246
3357
|
const transport = new StdioServerTransport();
|
|
4247
3358
|
await server.connect(transport);
|
|
4248
3359
|
console.error("MCP server running on stdio");
|